payfrit-works/portal/setup-wizard.html

2207 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
cursor: pointer;
transition: background 0.2s;
}
.modifier-header:hover {
background: var(--gray-50);
}
.modifier-header input[type="checkbox"] {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.modifier-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.modifier-name-row {
display: flex;
align-items: center;
gap: 12px;
}
.modifier-name {
font-weight: 600;
color: var(--gray-800);
font-size: 15px;
}
.modifier-type {
font-size: 11px;
color: var(--gray-500);
background: var(--gray-100);
padding: 2px 8px;
border-radius: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.modifier-meta {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.source-badge, .applies-to-badge, .options-count {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
background: var(--gray-100);
color: var(--gray-600);
}
.source-badge {
background: rgba(99, 102, 241, 0.1);
color: var(--primary);
}
.applies-to-badge {
background: rgba(34, 197, 94, 0.1);
color: var(--success);
}
.applies-to-badge.uncertain {
background: rgba(245, 158, 11, 0.1);
color: #d97706;
}
.expand-icon {
font-size: 12px;
color: var(--gray-400);
transition: transform 0.2s;
flex-shrink: 0;
}
.modifier-details {
border-top: 1px solid var(--gray-200);
background: var(--gray-50);
}
.modifier-options-list {
padding: 12px 16px 12px 46px;
display: flex;
flex-direction: column;
gap: 6px;
}
.modifier-option-detail {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #fff;
border: 1px solid var(--gray-200);
border-radius: 4px;
}
.option-name {
font-size: 14px;
color: var(--gray-700);
}
.option-price {
font-size: 13px;
color: var(--success);
font-weight: 500;
}
.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;
}
/* Category Selection */
.category-option {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #fff;
border: 1px solid var(--gray-200);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.category-option:hover {
border-color: var(--primary);
background: rgba(99, 102, 241, 0.05);
}
.category-option input[type="checkbox"] {
cursor: pointer;
}
.category-option span {
font-size: 14px;
color: var(--gray-700);
}
/* 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 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 -->
<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>
<!-- Image Preview Modal -->
<div id="imagePreviewModal" class="image-modal" onclick="closeImagePreview(event)">
<div class="image-modal-content">
<span class="image-modal-close" onclick="closeImagePreview()">&times;</span>
<img id="imagePreviewImg" src="" alt="Menu Image">
<div class="image-modal-caption" id="imagePreviewCaption"></div>
</div>
</div>
<style>
.image-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.9);
justify-content: center;
align-items: center;
}
.image-modal.active {
display: flex;
}
.image-modal-content {
position: relative;
max-width: 90%;
max-height: 90%;
}
.image-modal-content img {
max-width: 100%;
max-height: 85vh;
object-fit: contain;
border-radius: 8px;
}
.image-modal-close {
position: absolute;
top: -40px;
right: 0;
color: white;
font-size: 32px;
cursor: pointer;
padding: 5px 10px;
}
.image-modal-close:hover {
color: #ccc;
}
.image-modal-caption {
color: white;
text-align: center;
padding: 10px;
font-size: 14px;
}
.source-badge.clickable {
cursor: pointer;
text-decoration: underline;
color: var(--primary);
}
.source-badge.clickable:hover {
color: var(--primary-hover);
}
</style>
<script>
// Configuration
const config = {
businessId: null,
apiBaseUrl: '',
uploadedFiles: [],
extractedData: {
business: {},
categories: [],
modifiers: [],
items: []
},
currentStep: 1,
imageObjectUrls: [] // Store object URLs for uploaded images
};
// Image preview functions
function showImagePreview(imageIndex) {
// imageIndex is 1-based from the API
const fileIndex = imageIndex - 1;
if (fileIndex < 0 || fileIndex >= config.uploadedFiles.length) {
console.error('Invalid image index:', imageIndex);
return;
}
const file = config.uploadedFiles[fileIndex];
// Create object URL if not cached
if (!config.imageObjectUrls[fileIndex]) {
config.imageObjectUrls[fileIndex] = URL.createObjectURL(file);
}
const modal = document.getElementById('imagePreviewModal');
const img = document.getElementById('imagePreviewImg');
const caption = document.getElementById('imagePreviewCaption');
img.src = config.imageObjectUrls[fileIndex];
caption.textContent = `Image ${imageIndex}: ${file.name}`;
modal.classList.add('active');
// Prevent body scroll
document.body.style.overflow = 'hidden';
}
function closeImagePreview(event) {
// If event is passed, only close if clicking the background (not the image)
if (event && event.target.id !== 'imagePreviewModal') {
return;
}
const modal = document.getElementById('imagePreviewModal');
modal.classList.remove('active');
document.body.style.overflow = '';
}
// Close on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeImagePreview();
}
});
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initializeConfig();
setupUploadZone();
loadBusinessInfo();
});
function initializeConfig() {
// Wizard is for creating NEW businesses - businessId starts as null
config.businessId = null;
// Determine API base URL
const basePath = window.location.pathname.includes('/biz.payfrit.com/')
? '/biz.payfrit.com'
: '';
config.apiBaseUrl = basePath + '/api';
// 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() {
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' : ''}... This may take several minutes, please be patient.</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;
// 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 = '';
// 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';
}
// 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 open with defaults
for (let i = 0; i < 7; i++) {
schedule.push({ open: '09:00', close: '17:00', closed: false });
}
if (!hoursText || !hoursText.trim()) {
return schedule;
}
// Try to extract a time range
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);
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')}`;
};
const openTime = convertTo24Hour(timeMatch[1], timeMatch[2], timeMatch[3]);
const closeTime = convertTo24Hour(timeMatch[4], timeMatch[5], timeMatch[6]);
// Apply extracted time to all days
for (let i = 0; i < 7; i++) {
schedule[i] = { open: openTime, close: closeTime, closed: false };
}
}
return schedule;
}
// Toggle day closed/open - time inputs always stay editable
function toggleDayClosed(dayIdx) {
// No-op: time inputs stay editable so user can set hours before unchecking "Closed"
}
// Check for duplicate businesses before creating a new one
async function checkForDuplicateBusiness(biz) {
try {
const response = await fetch(`${config.apiBaseUrl}/setup/checkDuplicate.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: biz.name || '',
addressLine1: biz.addressLine1 || '',
city: biz.city || '',
state: biz.state || '',
zip: biz.zip || ''
})
});
const result = await response.json();
if (result.OK && result.duplicates && result.duplicates.length > 0) {
// Show duplicate warning
const dupList = result.duplicates.map(d =>
`<li><strong>${d.BusinessName}</strong> (ID: ${d.BusinessID})<br><small>${d.Address || 'No address'}</small></li>`
).join('');
addMessage('ai', `
<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 16px; margin-bottom: 16px;">
<p style="margin: 0 0 12px 0; font-weight: 600; color: #92400e;">
Possible duplicate found!
</p>
<p style="margin: 0 0 12px 0; color: #78350f;">
A business with a similar name or address already exists:
</p>
<ul style="margin: 0 0 16px 0; padding-left: 20px; color: #78350f;">
${dupList}
</ul>
<p style="margin: 0; color: #78350f; font-size: 14px;">
You can continue to create a new business, or go back and select the existing one from the portal.
</p>
</div>
`);
}
} catch (error) {
console.error('Error checking for duplicates:', error);
// Don't block the wizard if duplicate check fails
}
}
// Step 1: Business Info
async function showBusinessInfoStep() {
updateProgress(2);
const biz = config.extractedData.business || {};
// Check for duplicate businesses before showing the form
await checkForDuplicateBusiness(biz);
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()).filter(p => p.length > 0);
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;
}
}
// Clean up any trailing commas, periods, or whitespace from city
city = city.replace(/[,.\s]+$/, '').trim();
}
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" required>
</div>
<div class="extracted-value editable">
<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;">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;">
</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;">
</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">
<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() {
// Collect hours from the table - only include non-closed days
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;
// Only add days that are not closed
if (!isClosed) {
hoursSchedule.push({
day: dayNames[i],
dayId: i + 1, // 1=Monday, 7=Sunday
open: openTime,
close: closeTime
});
}
}
// Update stored data with any edits
config.extractedData.business = {
name: document.getElementById('bizName').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,
hoursSchedule: hoursSchedule // Send the structured schedule instead of the raw hours string
};
// 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 || [];
// 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>
<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) => {
const sourceImgIndex = mod.sourceImageIndex;
const sourceImgBadge = sourceImgIndex
? `<span class="source-badge clickable" onclick="event.stopPropagation(); showImagePreview(${sourceImgIndex})">Image ${sourceImgIndex}</span>`
: '<span class="source-badge">Unknown source</span>';
const appliesToInfo = mod.appliesTo === 'category' && mod.categoryName
? `<span class="applies-to-badge">Applies to: ${mod.categoryName}</span>`
: mod.appliesTo === 'uncertain'
? '<span class="applies-to-badge uncertain">Uncertain application</span>'
: '';
const optionsCount = (mod.options || []).length;
const optionsList = (mod.options || []).filter(opt => opt && opt.name).map(opt => `
<div class="modifier-option-detail">
<span class="option-name">${opt.name}</span>
<span class="option-price">${opt.price ? `+$${opt.price.toFixed(2)}` : '$0.00'}</span>
</div>
`).join('');
return `
<div class="modifier-template" data-index="${i}">
<div class="modifier-header" onclick="toggleModifierDetails(${i})">
<input type="checkbox" checked data-index="${i}" onclick="event.stopPropagation()">
<div class="modifier-info">
<div class="modifier-name-row">
<span class="modifier-name">${mod.name || 'Unnamed'}</span>
<span class="modifier-type">${mod.required ? 'Required' : 'Optional'}</span>
</div>
<div class="modifier-meta">
${sourceImgBadge}
${appliesToInfo}
<span class="options-count">${optionsCount} option${optionsCount !== 1 ? 's' : ''}</span>
</div>
</div>
<span class="expand-icon">▼</span>
</div>
<div class="modifier-details" id="mod-details-${i}" style="display: none;">
<div class="modifier-options-list">
${optionsList}
</div>
</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 toggleModifierDetails(index) {
const details = document.getElementById(`mod-details-${index}`);
const icon = details.previousElementSibling.querySelector('.expand-icon');
if (details.style.display === 'none') {
details.style.display = 'block';
icon.textContent = '▲';
} else {
details.style.display = 'none';
icon.textContent = '▼';
}
}
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;
// Go to items step (uncertain modifiers will be handled after items)
showItemsStep();
}
// New step: Handle uncertain modifier assignments
function showUncertainModifiersStep() {
const modifiers = config.extractedData.modifiers || [];
const categories = config.extractedData.categories || [];
// Find modifiers marked as "uncertain"
const uncertainModifiers = modifiers.filter(mod =>
mod.appliesTo === 'uncertain'
);
if (uncertainModifiers.length === 0 || categories.length === 0) {
// No uncertain modifiers or no categories, skip to final review
showFinalStep();
return;
}
// Initialize uncertain modifier assignment tracking
if (!config.uncertainModifierAssignments) {
config.uncertainModifierAssignments = {};
config.currentUncertainModIndex = 0;
}
const currentIndex = config.currentUncertainModIndex;
if (currentIndex >= uncertainModifiers.length) {
// All uncertain modifiers have been processed, apply assignments and continue
applyUncertainModifierAssignments();
showFinalStep();
return;
}
const modifier = uncertainModifiers[currentIndex];
// Build detailed modifier view
const sourceImgIndex = modifier.sourceImageIndex;
const sourceImgBadge = sourceImgIndex
? `<span class="source-badge clickable" onclick="showImagePreview(${sourceImgIndex})">Image ${sourceImgIndex}</span>`
: '<span class="source-badge">Unknown source</span>';
const optionsCount = (modifier.options || []).length;
const optionsList = (modifier.options || []).filter(opt => opt && opt.name).map(opt => `
<div class="modifier-option-detail">
<span class="option-name">${opt.name}</span>
<span class="option-price">${opt.price ? `+$${opt.price.toFixed(2)}` : '$0.00'}</span>
</div>
`).join('');
// Ask user about this modifier
const categoryOptions = categories.map((cat, i) => `
<label class="category-option">
<input type="checkbox" name="category-assign" value="${cat.name}">
<span>${cat.name}</span>
</label>
`).join('');
addMessage('ai', `
<p>I found the modifier template <strong>"${modifier.name}"</strong> but I'm not sure which items it applies to.</p>
<div class="modifier-details-view" style="background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 8px; padding: 16px; margin: 16px 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 500;">${modifier.name}</span>
${sourceImgBadge}
</div>
<div style="color: var(--gray-600); font-size: 14px; margin-bottom: 8px;">
<strong>${optionsCount} option${optionsCount !== 1 ? 's' : ''}</strong> • ${modifier.required ? 'Required' : 'Optional'}
</div>
<div class="modifier-options-list">
${optionsList}
</div>
</div>
<p>Select the categories where this modifier should be applied, or skip if it doesn't apply automatically:</p>
<div class="category-selection" style="display: flex; flex-direction: column; gap: 8px; margin: 16px 0;">
${categoryOptions}
</div>
<div class="action-buttons">
<button class="btn btn-outline" onclick="skipUncertainModifier()">Skip This Modifier</button>
<button class="btn btn-primary" onclick="assignUncertainModifier('${modifier.name}')">Apply to Selected Categories</button>
</div>
`);
}
function skipUncertainModifier() {
config.currentUncertainModIndex++;
showUncertainModifiersStep();
}
function assignUncertainModifier(modifierName) {
const checkboxes = document.querySelectorAll('input[name="category-assign"]:checked');
const selectedCategories = Array.from(checkboxes).map(cb => cb.value);
if (selectedCategories.length > 0) {
config.uncertainModifierAssignments[modifierName] = selectedCategories;
}
config.currentUncertainModIndex++;
showUncertainModifiersStep();
}
function applyUncertainModifierAssignments() {
// Apply the user's category selections to items
const items = config.extractedData.items || [];
const modifiers = config.extractedData.modifiers || [];
for (const [modifierName, categories] of Object.entries(config.uncertainModifierAssignments)) {
items.forEach(item => {
if (!item.modifiers) {
item.modifiers = [];
}
// If this item is in one of the selected categories, add the modifier
if (categories.includes(item.category)) {
if (!item.modifiers.includes(modifierName)) {
item.modifiers.push(modifierName);
}
}
});
// Update the modifier's appliesTo field to reflect the assignment
const modifier = modifiers.find(m => m.name === modifierName);
if (modifier) {
modifier.appliesTo = 'category';
modifier.categoryNames = categories; // Store all assigned categories
}
}
}
// 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())
);
// Now that user has seen items, ask about uncertain modifiers
showUncertainModifiersStep();
}
// 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() {
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...';
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 || 0,
userId: config.userId,
data: config.extractedData
})
});
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) {
const errorMsg = result.errors && result.errors.length > 0
? result.errors.join('; ')
: (result.MESSAGE || 'Save failed');
throw new Error(errorMsg);
}
showToast('Menu saved successfully!', 'success');
// 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#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>