payfrit-works/portal/setup-wizard.html
John Mizerek 8d5c0cc6ac 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>
2026-01-14 16:02:21 -08:00

1505 lines
47 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Menu Setup Wizard - Payfrit</title>
<link rel="stylesheet" href="portal.css">
<style>
/* Setup Wizard Specific Styles */
.wizard-container {
max-width: 900px;
margin: 0 auto;
}
.wizard-header {
text-align: center;
margin-bottom: 32px;
}
.wizard-header h1 {
font-size: 28px;
color: var(--gray-900);
margin-bottom: 8px;
}
.wizard-header p {
color: var(--gray-500);
font-size: 16px;
}
/* Upload Zone */
.upload-zone {
border: 2px dashed var(--gray-300);
border-radius: var(--radius);
padding: 48px 24px;
text-align: center;
background: var(--gray-50);
cursor: pointer;
transition: all 0.2s;
margin-bottom: 24px;
}
.upload-zone:hover, .upload-zone.dragover {
border-color: var(--primary);
background: rgba(99, 102, 241, 0.05);
}
.upload-zone.has-files {
border-style: solid;
border-color: var(--success);
background: rgba(34, 197, 94, 0.05);
}
.upload-icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
color: var(--gray-400);
}
.upload-zone:hover .upload-icon {
color: var(--primary);
}
.upload-zone h3 {
font-size: 18px;
color: var(--gray-700);
margin-bottom: 8px;
}
.upload-zone p {
color: var(--gray-500);
font-size: 14px;
}
.upload-zone input[type="file"] {
display: none;
}
/* File Preview Grid */
.file-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
margin-top: 16px;
}
.file-preview {
position: relative;
aspect-ratio: 1;
border-radius: var(--radius);
overflow: hidden;
background: var(--gray-200);
}
.file-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-preview .remove-file {
position: absolute;
top: 4px;
right: 4px;
width: 24px;
height: 24px;
background: rgba(0,0,0,0.6);
color: #fff;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.file-preview .remove-file:hover {
background: var(--danger);
}
.file-name {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.6);
color: #fff;
font-size: 11px;
padding: 4px 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Conversation Flow */
.conversation {
display: flex;
flex-direction: column;
gap: 20px;
}
.message {
display: flex;
gap: 12px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--primary);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 18px;
}
.message-avatar.ai {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
.message-content {
flex: 1;
background: #fff;
border-radius: var(--radius);
padding: 16px 20px;
box-shadow: var(--shadow);
}
.message-content p {
margin-bottom: 12px;
color: var(--gray-700);
line-height: 1.6;
}
.message-content p:last-child {
margin-bottom: 0;
}
/* Extracted Value Display */
.extracted-value {
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: var(--radius);
padding: 12px 16px;
margin: 12px 0;
font-weight: 500;
color: var(--gray-900);
}
.extracted-value.editable {
cursor: text;
}
.extracted-value input {
border: none;
background: transparent;
font-size: inherit;
font-weight: inherit;
color: inherit;
width: 100%;
outline: none;
}
/* Extracted List */
.extracted-list {
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: var(--radius);
margin: 12px 0;
max-height: 300px;
overflow-y: auto;
}
.extracted-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
border-bottom: 1px solid var(--gray-200);
}
.extracted-list-item:last-child {
border-bottom: none;
}
.extracted-list-item input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.extracted-list-item .item-text {
flex: 1;
font-weight: 500;
color: var(--gray-800);
}
.extracted-list-item .item-text input {
border: none;
background: transparent;
font-size: inherit;
font-weight: inherit;
color: inherit;
width: 100%;
outline: none;
}
.extracted-list-item .item-count {
font-size: 12px;
color: var(--gray-500);
background: var(--gray-200);
padding: 2px 8px;
border-radius: 10px;
}
.extracted-list-item .remove-item {
color: var(--gray-400);
background: none;
border: none;
cursor: pointer;
padding: 4px;
}
.extracted-list-item .remove-item:hover {
color: var(--danger);
}
/* Modifier Template Display */
.modifier-template {
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: var(--radius);
margin: 8px 0;
overflow: hidden;
}
.modifier-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid var(--gray-200);
cursor: pointer;
}
.modifier-header input[type="checkbox"] {
width: 18px;
height: 18px;
}
.modifier-name {
flex: 1;
font-weight: 600;
color: var(--gray-800);
}
.modifier-type {
font-size: 12px;
color: var(--gray-500);
background: var(--gray-100);
padding: 2px 8px;
border-radius: 10px;
}
.modifier-options {
padding: 8px 16px 12px 46px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.modifier-option {
background: #fff;
border: 1px solid var(--gray-200);
border-radius: 4px;
padding: 4px 10px;
font-size: 13px;
color: var(--gray-600);
}
.modifier-option .price {
color: var(--success);
margin-left: 4px;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 8px;
margin-top: 16px;
}
.action-buttons .btn {
flex: 1;
}
.btn-success {
background: var(--success);
color: #fff;
}
.btn-success:hover {
background: #16a34a;
}
.btn-outline {
background: #fff;
border: 1px solid var(--gray-300);
color: var(--gray-700);
}
.btn-outline:hover {
background: var(--gray-50);
border-color: var(--gray-400);
}
/* Progress Indicator */
.progress-steps {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 32px;
}
.progress-step {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--gray-200);
transition: all 0.3s;
}
.progress-step.active {
background: var(--primary);
transform: scale(1.2);
}
.progress-step.completed {
background: var(--success);
}
/* Loading State */
.loading-indicator {
display: flex;
align-items: center;
gap: 12px;
padding: 20px;
color: var(--gray-600);
}
.loading-spinner {
width: 24px;
height: 24px;
border: 3px solid var(--gray-200);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Items Table */
.items-table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
font-size: 14px;
}
.items-table th {
text-align: left;
padding: 10px 12px;
background: var(--gray-100);
font-weight: 600;
color: var(--gray-600);
font-size: 12px;
text-transform: uppercase;
}
.items-table td {
padding: 10px 12px;
border-bottom: 1px solid var(--gray-100);
vertical-align: top;
}
.items-table tr:hover {
background: var(--gray-50);
}
.item-modifiers {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.item-modifier-tag {
font-size: 11px;
background: var(--primary-light);
color: #fff;
padding: 2px 6px;
border-radius: 3px;
}
/* Add Item Row */
.add-row {
display: flex;
gap: 8px;
padding: 12px;
background: var(--gray-50);
border-radius: var(--radius);
margin-top: 8px;
}
.add-row input {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--gray-300);
border-radius: var(--radius);
font-size: 14px;
}
/* Summary Card */
.summary-card {
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 16px;
}
.summary-card-header {
padding: 16px 20px;
background: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
display: flex;
align-items: center;
justify-content: space-between;
}
.summary-card-header h3 {
font-size: 16px;
font-weight: 600;
color: var(--gray-800);
}
.summary-card-body {
padding: 16px 20px;
}
.summary-stat {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--gray-100);
}
.summary-stat:last-child {
border-bottom: none;
}
.summary-stat-label {
color: var(--gray-600);
}
.summary-stat-value {
font-weight: 600;
color: var(--gray-900);
}
/* Back Link */
.back-link {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--gray-500);
text-decoration: none;
font-size: 14px;
margin-bottom: 24px;
}
.back-link:hover {
color: var(--gray-700);
}
/* Hidden */
.hidden {
display: none !important;
}
/* Scrollable Items Section */
.items-section {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--gray-200);
border-radius: var(--radius);
}
</style>
</head>
<body>
<div class="app">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="logo">
<div class="logo-icon">P</div>
<span class="logo-text">Payfrit</span>
</div>
<button class="sidebar-toggle" onclick="toggleSidebar()">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12h18M3 6h18M3 18h18"/>
</svg>
</button>
</div>
<nav class="sidebar-nav">
<a href="index.html#dashboard" class="nav-item">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>
<span>Dashboard</span>
</a>
<a href="index.html#menu" class="nav-item">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 19h16M4 15h16M4 11h16M4 7h16"/>
</svg>
<span>Menu</span>
</a>
<a href="setup-wizard.html" class="nav-item active">
<svg class="nav-icon" 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>
<span>Setup Wizard</span>
</a>
</nav>
<div class="sidebar-footer">
<div class="business-info">
<div class="business-avatar" id="businessAvatar">?</div>
<div class="business-details">
<div class="business-name" id="businessName">Loading...</div>
<div class="business-status online">Online</div>
</div>
</div>
<a href="login.html" class="nav-item logout" onclick="logout()">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16,17 21,12 16,7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
<span>Logout</span>
</a>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<div class="top-bar">
<div class="page-title">
<h1>Menu Setup Wizard</h1>
</div>
<div class="top-bar-actions">
<button class="btn btn-secondary" onclick="openPreview()" id="previewBtn" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
Preview Menu
</button>
</div>
</div>
<div class="page-container">
<div class="wizard-container">
<!-- Back Link -->
<a href="index.html#menu" class="back-link">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
Back to Menu
</a>
<!-- Progress Steps -->
<div class="progress-steps">
<div class="progress-step active" data-step="1"></div>
<div class="progress-step" data-step="2"></div>
<div class="progress-step" data-step="3"></div>
<div class="progress-step" data-step="4"></div>
<div class="progress-step" data-step="5"></div>
<div class="progress-step" data-step="6"></div>
</div>
<!-- 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>
</div>
<!-- Upload Section -->
<div id="uploadSection">
<div class="upload-zone" id="uploadZone">
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<h3>Drop your menu images here</h3>
<p>or click to browse (JPG, PNG, PDF supported)</p>
<input type="file" id="fileInput" multiple accept="image/*,.pdf">
</div>
<div class="file-preview-grid" id="filePreviewGrid"></div>
<div class="action-buttons" id="uploadActions" style="display: none;">
<button class="btn btn-primary" onclick="startAnalysis()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
Analyze Menu
</button>
</div>
</div>
<!-- Conversation Section -->
<div class="conversation" id="conversation"></div>
<!-- Final Actions -->
<div id="finalActions" class="hidden" style="margin-top: 24px;">
<div class="summary-card">
<div class="summary-card-header">
<h3>Menu Summary</h3>
</div>
<div class="summary-card-body">
<div class="summary-stat">
<span class="summary-stat-label">Categories</span>
<span class="summary-stat-value" id="summaryCategories">0</span>
</div>
<div class="summary-stat">
<span class="summary-stat-label">Modifier Templates</span>
<span class="summary-stat-value" id="summaryModifiers">0</span>
</div>
<div class="summary-stat">
<span class="summary-stat-label">Menu Items</span>
<span class="summary-stat-value" id="summaryItems">0</span>
</div>
</div>
</div>
<div class="action-buttons">
<button class="btn btn-outline" onclick="startOver()">Start Over</button>
<button class="btn btn-success" onclick="saveMenu()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save Menu
</button>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toastContainer"></div>
<script>
// Configuration
const config = {
businessId: null,
apiBaseUrl: '',
uploadedFiles: [],
extractedData: {
business: {},
categories: [],
modifiers: [],
items: []
},
currentStep: 1
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initializeConfig();
setupUploadZone();
loadBusinessInfo();
});
function initializeConfig() {
// Get business ID from URL or localStorage
const urlParams = new URLSearchParams(window.location.search);
config.businessId = urlParams.get('bid') || localStorage.getItem('payfrit_portal_business');
// Determine API base URL
const basePath = window.location.pathname.includes('/biz.payfrit.com/')
? '/biz.payfrit.com'
: '';
config.apiBaseUrl = basePath + '/api';
if (!config.businessId) {
window.location.href = 'login.html';
}
}
function setupUploadZone() {
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
}
function handleFiles(files) {
const uploadZone = document.getElementById('uploadZone');
const previewGrid = document.getElementById('filePreviewGrid');
const uploadActions = document.getElementById('uploadActions');
Array.from(files).forEach(file => {
if (!file.type.match('image.*') && file.type !== 'application/pdf') {
showToast('Only images and PDFs are supported', 'error');
return;
}
config.uploadedFiles.push(file);
// Create preview
const preview = document.createElement('div');
preview.className = 'file-preview';
preview.dataset.index = config.uploadedFiles.length - 1;
if (file.type.match('image.*')) {
const reader = new FileReader();
reader.onload = (e) => {
preview.innerHTML = `
<img src="${e.target.result}" alt="${file.name}">
<button class="remove-file" onclick="removeFile(${config.uploadedFiles.length - 1})">&times;</button>
<div class="file-name">${file.name}</div>
`;
};
reader.readAsDataURL(file);
} else {
preview.innerHTML = `
<div style="display:flex;align-items:center;justify-content:center;height:100%;background:var(--gray-100);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14,2 14,8 20,8"/>
</svg>
</div>
<button class="remove-file" onclick="removeFile(${config.uploadedFiles.length - 1})">&times;</button>
<div class="file-name">${file.name}</div>
`;
}
previewGrid.appendChild(preview);
});
if (config.uploadedFiles.length > 0) {
uploadZone.classList.add('has-files');
uploadActions.style.display = 'flex';
}
}
function removeFile(index) {
config.uploadedFiles.splice(index, 1);
rebuildPreviewGrid();
}
function rebuildPreviewGrid() {
const previewGrid = document.getElementById('filePreviewGrid');
const uploadZone = document.getElementById('uploadZone');
const uploadActions = document.getElementById('uploadActions');
previewGrid.innerHTML = '';
config.uploadedFiles.forEach((file, index) => {
const preview = document.createElement('div');
preview.className = 'file-preview';
preview.dataset.index = index;
if (file.type.match('image.*')) {
const reader = new FileReader();
reader.onload = (e) => {
preview.innerHTML = `
<img src="${e.target.result}" alt="${file.name}">
<button class="remove-file" onclick="removeFile(${index})">&times;</button>
<div class="file-name">${file.name}</div>
`;
};
reader.readAsDataURL(file);
} else {
preview.innerHTML = `
<div style="display:flex;align-items:center;justify-content:center;height:100%;background:var(--gray-100);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14,2 14,8 20,8"/>
</svg>
</div>
<button class="remove-file" onclick="removeFile(${index})">&times;</button>
<div class="file-name">${file.name}</div>
`;
}
previewGrid.appendChild(preview);
});
if (config.uploadedFiles.length === 0) {
uploadZone.classList.remove('has-files');
uploadActions.style.display = 'none';
}
}
async function startAnalysis() {
if (config.uploadedFiles.length === 0) {
showToast('Please upload at least one menu image', 'error');
return;
}
// Hide upload section, show conversation
document.getElementById('uploadSection').style.display = 'none';
// Add loading message
addMessage('ai', `
<div class="loading-indicator">
<div class="loading-spinner"></div>
<span>Analyzing ${config.uploadedFiles.length} menu image${config.uploadedFiles.length > 1 ? 's' : ''}...</span>
</div>
`);
try {
// Send images to API
const formData = new FormData();
config.uploadedFiles.forEach((file, i) => {
formData.append('file' + i, file);
});
formData.append('businessId', config.businessId);
const response = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuImages.cfm`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.OK) {
throw new Error(result.MESSAGE || 'Analysis failed');
}
// Store extracted data
config.extractedData = result.DATA;
// Remove loading message and start conversation flow
document.getElementById('conversation').innerHTML = '';
// Start with business info
showBusinessInfoStep();
} catch (error) {
console.error('Analysis error:', error);
document.getElementById('conversation').innerHTML = '';
addMessage('ai', `
<p>Sorry, I encountered an error analyzing your menu:</p>
<p style="color: var(--danger);">${error.message}</p>
<div class="action-buttons">
<button class="btn btn-primary" onclick="retryAnalysis()">Try Again</button>
</div>
`);
}
}
function retryAnalysis() {
document.getElementById('conversation').innerHTML = '';
document.getElementById('uploadSection').style.display = 'block';
}
// Step 1: Business Info
function showBusinessInfoStep() {
updateProgress(2);
const biz = config.extractedData.business || {};
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">
</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">
</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">
</div>
<p>Is this information correct?</p>
<div class="action-buttons">
<button class="btn btn-success" onclick="confirmBusinessInfo()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
Looks Good
</button>
</div>
`);
}
function confirmBusinessInfo() {
// Update stored data with any edits
config.extractedData.business = {
name: document.getElementById('bizName').value,
address: document.getElementById('bizAddress').value,
phone: document.getElementById('bizPhone').value,
hours: document.getElementById('bizHours').value
};
// Move to categories
showCategoriesStep();
}
// Step 2: Categories
function showCategoriesStep() {
updateProgress(3);
const categories = config.extractedData.categories || [];
let categoriesHtml = categories.map((cat, i) => `
<div class="extracted-list-item">
<input type="checkbox" checked data-index="${i}">
<span class="item-text">
<input type="text" value="${cat.name}" data-index="${i}">
</span>
<span class="item-count">${cat.itemCount || 0} items</span>
<button class="remove-item" onclick="removeCategory(${i})">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
`).join('');
addMessage('ai', `
<p>I found <strong>${categories.length} menu categories</strong>:</p>
<div class="extracted-list" id="categoriesList">
${categoriesHtml}
</div>
<div class="add-row">
<input type="text" id="newCategoryName" placeholder="Add new category...">
<button class="btn btn-secondary" onclick="addCategory()">Add</button>
</div>
<p>Uncheck any categories you don't want to include.</p>
<div class="action-buttons">
<button class="btn btn-success" onclick="confirmCategories()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
Continue
</button>
</div>
`);
}
function removeCategory(index) {
config.extractedData.categories.splice(index, 1);
// Rebuild the list
const list = document.getElementById('categoriesList');
const categories = config.extractedData.categories;
list.innerHTML = categories.map((cat, i) => `
<div class="extracted-list-item">
<input type="checkbox" checked data-index="${i}">
<span class="item-text">
<input type="text" value="${cat.name}" data-index="${i}">
</span>
<span class="item-count">${cat.itemCount || 0} items</span>
<button class="remove-item" onclick="removeCategory(${i})">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
`).join('');
}
function addCategory() {
const input = document.getElementById('newCategoryName');
const name = input.value.trim();
if (!name) return;
config.extractedData.categories.push({ name, itemCount: 0 });
input.value = '';
// Rebuild list
removeCategory(-1); // Hacky way to rebuild without removing
config.extractedData.categories.pop(); // Undo the splice
const list = document.getElementById('categoriesList');
const categories = config.extractedData.categories;
list.innerHTML = categories.map((cat, i) => `
<div class="extracted-list-item">
<input type="checkbox" checked data-index="${i}">
<span class="item-text">
<input type="text" value="${cat.name}" data-index="${i}">
</span>
<span class="item-count">${cat.itemCount || 0} items</span>
<button class="remove-item" onclick="removeCategory(${i})">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
`).join('');
}
function confirmCategories() {
// Update categories with any edits and filter unchecked
const list = document.getElementById('categoriesList');
const items = list.querySelectorAll('.extracted-list-item');
const updatedCategories = [];
items.forEach(item => {
const checkbox = item.querySelector('input[type="checkbox"]');
const nameInput = item.querySelector('.item-text input');
if (checkbox.checked) {
updatedCategories.push({
name: nameInput.value,
itemCount: config.extractedData.categories[checkbox.dataset.index]?.itemCount || 0
});
}
});
config.extractedData.categories = updatedCategories;
showModifiersStep();
}
// Step 3: Modifiers
function showModifiersStep() {
updateProgress(4);
const modifiers = config.extractedData.modifiers || [];
if (modifiers.length === 0) {
addMessage('ai', `
<p>I didn't detect any modifier templates (like size options, add-ons, etc.).</p>
<p>Would you like to add some manually, or continue without modifiers?</p>
<div class="action-buttons">
<button class="btn btn-outline" onclick="showItemsStep()">Skip Modifiers</button>
<button class="btn btn-primary" onclick="showAddModifierForm()">Add Modifiers</button>
</div>
`);
return;
}
let modifiersHtml = modifiers.map((mod, i) => `
<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-type">${mod.required ? 'Required' : 'Optional'}</span>
</div>
<div class="modifier-options">
${(mod.options || []).map(opt => `
<span class="modifier-option">
${opt.name}${opt.price ? `<span class="price">+$${opt.price.toFixed(2)}</span>` : ''}
</span>
`).join('')}
</div>
</div>
`).join('');
addMessage('ai', `
<p>I found <strong>${modifiers.length} modifier templates</strong> that can be applied to menu items:</p>
<div id="modifiersList">
${modifiersHtml}
</div>
<p>Uncheck any you don't want to use.</p>
<div class="action-buttons">
<button class="btn btn-success" onclick="confirmModifiers()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
Continue
</button>
</div>
`);
}
function confirmModifiers() {
const list = document.getElementById('modifiersList');
const templates = list.querySelectorAll('.modifier-template');
const updatedModifiers = [];
templates.forEach(template => {
const checkbox = template.querySelector('input[type="checkbox"]');
if (checkbox.checked) {
const index = parseInt(checkbox.dataset.index);
updatedModifiers.push(config.extractedData.modifiers[index]);
}
});
config.extractedData.modifiers = updatedModifiers;
showItemsStep();
}
// Step 4: Items
function showItemsStep() {
updateProgress(5);
const items = config.extractedData.items || [];
const categories = config.extractedData.categories || [];
if (items.length === 0) {
addMessage('ai', `
<p>I couldn't extract any menu items. This might happen with complex or handwritten menus.</p>
<p>You can add items manually in the Menu Builder after completing this wizard.</p>
<div class="action-buttons">
<button class="btn btn-success" onclick="showFinalStep()">Continue to Review</button>
</div>
`);
return;
}
// Group items by category
let itemsByCategory = {};
categories.forEach(cat => {
itemsByCategory[cat.name] = items.filter(item => item.category === cat.name);
});
let itemsHtml = '';
for (const [catName, catItems] of Object.entries(itemsByCategory)) {
if (catItems.length === 0) continue;
itemsHtml += `
<div style="margin-bottom: 16px;">
<h4 style="margin-bottom: 8px; color: var(--gray-700);">${catName} (${catItems.length})</h4>
<table class="items-table">
<thead>
<tr>
<th style="width:40px;"></th>
<th>Item</th>
<th style="width:80px;">Price</th>
<th>Modifiers</th>
</tr>
</thead>
<tbody>
${catItems.map((item, i) => `
<tr>
<td><input type="checkbox" checked data-item-id="${item.id || i}"></td>
<td>
<strong>${item.name}</strong>
${item.description ? `<br><small style="color:var(--gray-500);">${item.description}</small>` : ''}
</td>
<td>$${(item.price || 0).toFixed(2)}</td>
<td>
<div class="item-modifiers">
${(item.modifiers || []).map(m => `<span class="item-modifier-tag">${m}</span>`).join('')}
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
addMessage('ai', `
<p>I found <strong>${items.length} menu items</strong> across your categories:</p>
<div class="items-section" id="itemsList">
${itemsHtml}
</div>
<p>Uncheck any items you don't want to include.</p>
<div class="action-buttons">
<button class="btn btn-success" onclick="confirmItems()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
Continue to Review
</button>
</div>
`);
}
function confirmItems() {
// Filter out unchecked items
const checkboxes = document.querySelectorAll('#itemsList input[type="checkbox"]');
const checkedIds = new Set();
checkboxes.forEach(cb => {
if (cb.checked) {
checkedIds.add(cb.dataset.itemId);
}
});
config.extractedData.items = config.extractedData.items.filter((item, i) =>
checkedIds.has(item.id || i.toString())
);
showFinalStep();
}
// Step 5: Final Review
function showFinalStep() {
updateProgress(6);
const { business, categories, modifiers, items } = config.extractedData;
document.getElementById('summaryCategories').textContent = categories.length;
document.getElementById('summaryModifiers').textContent = modifiers.length;
document.getElementById('summaryItems').textContent = items.length;
addMessage('ai', `
<p>Your menu is ready to save!</p>
<p><strong>${business.name || 'Your Restaurant'}</strong></p>
<ul style="margin: 12px 0; padding-left: 20px; color: var(--gray-600);">
<li>${categories.length} categories</li>
<li>${modifiers.length} modifier templates</li>
<li>${items.length} menu items</li>
</ul>
<p>Click "Save Menu" below to add everything to your Payfrit account.</p>
`);
document.getElementById('finalActions').classList.remove('hidden');
document.getElementById('previewBtn').disabled = false;
}
async function saveMenu() {
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...';
saveBtn.disabled = true;
try {
const response = await fetch(`${config.apiBaseUrl}/setup/saveWizard.cfm`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
businessId: config.businessId,
data: config.extractedData
})
});
const result = await response.json();
if (!result.OK) {
throw new Error(result.MESSAGE || 'Save failed');
}
showToast('Menu saved successfully!', 'success');
// Redirect to menu page after a moment
setTimeout(() => {
window.location.href = `index.html?bid=${config.businessId}#menu`;
}, 1500);
} catch (error) {
console.error('Save error:', error);
showToast('Failed to save: ' + error.message, 'error');
saveBtn.innerHTML = originalText;
saveBtn.disabled = false;
}
}
function startOver() {
if (!confirm('Are you sure you want to start over? All extracted data will be lost.')) {
return;
}
config.uploadedFiles = [];
config.extractedData = { business: {}, categories: [], modifiers: [], items: [] };
config.currentStep = 1;
document.getElementById('conversation').innerHTML = '';
document.getElementById('filePreviewGrid').innerHTML = '';
document.getElementById('uploadSection').style.display = 'block';
document.getElementById('uploadZone').classList.remove('has-files');
document.getElementById('uploadActions').style.display = 'none';
document.getElementById('finalActions').classList.add('hidden');
document.getElementById('previewBtn').disabled = true;
updateProgress(1);
}
function openPreview() {
const { business, categories, modifiers, items } = config.extractedData;
// Build preview HTML
let previewHtml = `
<!DOCTYPE html>
<html>
<head>
<title>Menu Preview - ${business.name || 'Restaurant'}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
h1 { border-bottom: 2px solid #333; padding-bottom: 10px; }
h2 { color: #6366f1; margin-top: 24px; cursor: pointer; }
h2:hover { color: #4f46e5; }
.category { margin-bottom: 24px; }
.category-items { padding-left: 20px; }
.item { padding: 12px 0; border-bottom: 1px solid #eee; }
.item-header { display: flex; justify-content: space-between; }
.item-name { font-weight: 600; }
.item-price { color: #22c55e; font-weight: 600; }
.item-desc { color: #666; font-size: 14px; margin-top: 4px; }
.item-mods { margin-top: 8px; }
.mod-tag { display: inline-block; background: #e0e7ff; color: #4338ca; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-right: 4px; }
.business-info { background: #f3f4f6; padding: 16px; border-radius: 8px; margin-bottom: 24px; }
.collapsed .category-items { display: none; }
.toggle-icon { float: right; }
</style>
</head>
<body>
<h1>${business.name || 'Menu Preview'}</h1>
<div class="business-info">
${business.address ? `<div><strong>Address:</strong> ${business.address}</div>` : ''}
${business.phone ? `<div><strong>Phone:</strong> ${business.phone}</div>` : ''}
${business.hours ? `<div><strong>Hours:</strong> ${business.hours}</div>` : ''}
</div>
`;
categories.forEach(cat => {
const catItems = items.filter(item => item.category === cat.name);
previewHtml += `
<div class="category">
<h2 onclick="this.parentElement.classList.toggle('collapsed')">
${cat.name} <span class="toggle-icon">[+/-]</span>
</h2>
<div class="category-items">
${catItems.map(item => `
<div class="item">
<div class="item-header">
<span class="item-name">${item.name}</span>
<span class="item-price">$${(item.price || 0).toFixed(2)}</span>
</div>
${item.description ? `<div class="item-desc">${item.description}</div>` : ''}
${item.modifiers && item.modifiers.length > 0 ? `
<div class="item-mods">
${item.modifiers.map(m => `<span class="mod-tag">${m}</span>`).join('')}
</div>
` : ''}
</div>
`).join('')}
${catItems.length === 0 ? '<div class="item" style="color:#999;">No items in this category</div>' : ''}
</div>
</div>
`;
});
previewHtml += `
</body>
</html>
`;
const previewWindow = window.open('', 'MenuPreview', 'width=900,height=700');
previewWindow.document.write(previewHtml);
previewWindow.document.close();
}
// Helper functions
function addMessage(type, content) {
const conversation = document.getElementById('conversation');
const avatar = type === 'ai'
? '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"/><path d="M12 6v6l4 2"/></svg>'
: 'U';
const message = document.createElement('div');
message.className = 'message';
message.innerHTML = `
<div class="message-avatar ${type}">${avatar}</div>
<div class="message-content">${content}</div>
`;
conversation.appendChild(message);
// Scroll to bottom
message.scrollIntoView({ behavior: 'smooth' });
}
function updateProgress(step) {
config.currentStep = step;
document.querySelectorAll('.progress-step').forEach(el => {
const s = parseInt(el.dataset.step);
el.classList.remove('active', 'completed');
if (s < step) el.classList.add('completed');
if (s === step) el.classList.add('active');
});
}
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
async function loadBusinessInfo() {
try {
const response = await fetch(`${config.apiBaseUrl}/businesses/get.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ BusinessID: config.businessId })
});
const result = await response.json();
if (result.OK && result.BUSINESS) {
const biz = result.BUSINESS;
document.getElementById('businessName').textContent = biz.NAME || 'Business';
document.getElementById('businessAvatar').textContent = (biz.NAME || 'B')[0].toUpperCase();
}
} catch (e) {
console.error('Failed to load business info:', e);
}
}
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('collapsed');
}
function logout() {
localStorage.removeItem('payfrit_portal_token');
localStorage.removeItem('payfrit_portal_userid');
localStorage.removeItem('payfrit_portal_business');
window.location.href = 'login.html';
}
</script>
</body>
</html>