- 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>
1505 lines
47 KiB
HTML
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})">×</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})">×</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})">×</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})">×</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>
|