Backend (analyzeMenuImages.cfm): - Track which image each modifier was found on via sourceImageIndex - Changed loop to use index to capture image number Frontend (setup-wizard.html): - Show detailed modifier information in expandable cards - Display source image number for each modifier - Show appliesTo status (category/uncertain) with badges - Display all modifier options with prices in detail view - Click modifier header to expand/collapse full option list - Added comprehensive CSS for new modifier display: * Expandable card design with hover effects * Color-coded badges for source image, category assignment * Detailed option list with prices * Visual distinction for uncertain modifiers Users can now: - See which image each modifier came from - View complete option list before confirming - Understand how each modifier will be applied - Make informed decisions about which modifiers to keep Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2074 lines
67 KiB
HTML
2074 lines
67 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;
|
||
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>
|
||
|
||
<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 (optional for new business setup)
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
config.businessId = urlParams.get('bid') || localStorage.getItem('payfrit_portal_business') || 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})">×</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' : ''}... 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 closed
|
||
for (let i = 0; i < 7; i++) {
|
||
schedule.push({ open: '09:00', close: '17:00', closed: true });
|
||
}
|
||
|
||
if (!hoursText || !hoursText.trim()) {
|
||
return schedule;
|
||
}
|
||
|
||
hoursText = hoursText.toLowerCase();
|
||
|
||
// Extract time range - find first time pattern
|
||
const timePattern = /(\d{1,2}):?(\d{2})?\s*(am|pm|a\.m\.|p\.m\.)?\s*(?:to|[-–])\s*(\d{1,2}):?(\d{2})?\s*(am|pm|a\.m\.|p\.m\.)?/i;
|
||
const timeMatch = hoursText.match(timePattern);
|
||
|
||
let openTime = '09:00';
|
||
let closeTime = '17:00';
|
||
|
||
if (timeMatch) {
|
||
const convertTo24Hour = (hour, minute, ampm) => {
|
||
let h = parseInt(hour);
|
||
const m = minute ? parseInt(minute) : 0;
|
||
|
||
if (ampm) {
|
||
ampm = ampm.replace(/\./g, '').toLowerCase();
|
||
if (ampm === 'pm' && h < 12) h += 12;
|
||
if (ampm === 'am' && h === 12) h = 0;
|
||
}
|
||
|
||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||
};
|
||
|
||
openTime = convertTo24Hour(timeMatch[1], timeMatch[2], timeMatch[3]);
|
||
closeTime = convertTo24Hour(timeMatch[4], timeMatch[5], timeMatch[6]);
|
||
}
|
||
|
||
// Find which days are mentioned
|
||
const dayPattern = /(mon|monday|tue|tuesday|tues|wed|wednesday|thu|thursday|thur|thurs|fri|friday|sat|saturday|sun|sunday)/gi;
|
||
const dayMatches = hoursText.match(dayPattern) || [];
|
||
|
||
if (dayMatches.length === 0) {
|
||
// No days mentioned, assume all days open
|
||
for (let i = 0; i < 7; i++) {
|
||
schedule[i] = { open: openTime, close: closeTime, closed: false };
|
||
}
|
||
return schedule;
|
||
}
|
||
|
||
// Process day ranges and individual days
|
||
const daysSet = new Set();
|
||
let i = 0;
|
||
while (i < dayMatches.length) {
|
||
const currentDay = dayMatches[i].toLowerCase();
|
||
const currentDayIdx = dayMap[currentDay];
|
||
|
||
// Check if next day forms a range
|
||
if (i < dayMatches.length - 1) {
|
||
const nextDay = dayMatches[i + 1].toLowerCase();
|
||
const betweenPattern = new RegExp(currentDay + '\\s*[-–]\\s*' + nextDay, 'i');
|
||
|
||
if (betweenPattern.test(hoursText)) {
|
||
// It's a range
|
||
const nextDayIdx = dayMap[nextDay];
|
||
for (let d = currentDayIdx; d <= nextDayIdx; d++) {
|
||
daysSet.add(d);
|
||
}
|
||
i += 2;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Individual day
|
||
daysSet.add(currentDayIdx);
|
||
i++;
|
||
}
|
||
|
||
// Apply times to the days found
|
||
daysSet.forEach(dayIdx => {
|
||
schedule[dayIdx] = { open: openTime, close: closeTime, closed: false };
|
||
});
|
||
|
||
return schedule;
|
||
}
|
||
|
||
// Toggle day closed/open
|
||
function toggleDayClosed(dayIdx) {
|
||
const closedCheckbox = document.getElementById(`closed_${dayIdx}`);
|
||
const openInput = document.getElementById(`open_${dayIdx}`);
|
||
const closeInput = document.getElementById(`close_${dayIdx}`);
|
||
|
||
if (closedCheckbox.checked) {
|
||
openInput.disabled = true;
|
||
closeInput.disabled = true;
|
||
} else {
|
||
openInput.disabled = false;
|
||
closeInput.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Step 1: Business Info
|
||
function showBusinessInfoStep() {
|
||
updateProgress(2);
|
||
const biz = config.extractedData.business || {};
|
||
|
||
console.log('Business data:', biz);
|
||
|
||
// Parse address into components if it's a single string
|
||
let addressLine1 = biz.addressLine1 || '';
|
||
let city = biz.city || '';
|
||
let state = biz.state || '';
|
||
let zip = biz.zip || '';
|
||
|
||
if (biz.address && !addressLine1) {
|
||
console.log('Parsing address:', biz.address);
|
||
|
||
// Parse from the end forward: ZIP (5 digits), then State (2 letters), then City + AddressLine1
|
||
let remaining = biz.address.trim();
|
||
|
||
// Extract ZIP (last 5 digits, optionally with -4)
|
||
const zipMatch = remaining.match(/\b(\d{5})(?:-\d{4})?\s*$/);
|
||
if (zipMatch) {
|
||
zip = zipMatch[1];
|
||
remaining = remaining.substring(0, zipMatch.index).trim();
|
||
console.log('Found ZIP:', zip, 'Remaining:', remaining);
|
||
}
|
||
|
||
// Extract State (2 letters before ZIP)
|
||
const stateMatch = remaining.match(/\b([A-Z]{2})\s*$/i);
|
||
if (stateMatch) {
|
||
state = stateMatch[1].toUpperCase();
|
||
remaining = remaining.substring(0, stateMatch.index).trim();
|
||
console.log('Found State:', state, 'Remaining:', remaining);
|
||
}
|
||
|
||
// What's left is AddressLine1 + City
|
||
// Try to split by comma first
|
||
if (remaining.includes(',')) {
|
||
const parts = remaining.split(',').map(p => p.trim());
|
||
addressLine1 = parts[0];
|
||
city = parts.slice(1).join(', ');
|
||
} else {
|
||
// No comma - try to find last sequence of words as city
|
||
// Cities are usually 1-3 words at the end
|
||
const words = remaining.split(/\s+/);
|
||
if (words.length > 3) {
|
||
// Assume last 2 words are city, rest is address
|
||
city = words.slice(-2).join(' ');
|
||
addressLine1 = words.slice(0, -2).join(' ');
|
||
} else if (words.length > 1) {
|
||
// Assume last word is city
|
||
city = words[words.length - 1];
|
||
addressLine1 = words.slice(0, -1).join(' ');
|
||
} else {
|
||
// All in address line 1
|
||
addressLine1 = remaining;
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('Parsed address:', { addressLine1, city, state, zip });
|
||
|
||
// Parse hours into a 7-day schedule
|
||
const hoursSchedule = parseHoursString(biz.hours || '');
|
||
|
||
addMessage('ai', `
|
||
<p>I found your restaurant information:</p>
|
||
<div class="extracted-value editable">
|
||
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Restaurant Name</label>
|
||
<input type="text" id="bizName" value="${biz.name || ''}" placeholder="Restaurant name" 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;"
|
||
${dayData.closed ? 'disabled' : ''}>
|
||
</td>
|
||
<td style="padding:8px;">
|
||
<input type="time" id="close_${idx}" value="${dayData.close}"
|
||
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;"
|
||
${dayData.closed ? 'disabled' : ''}>
|
||
</td>
|
||
<td style="padding:8px;text-align:center;">
|
||
<input type="checkbox" id="closed_${idx}" ${dayData.closed ? 'checked' : ''}
|
||
onchange="toggleDayClosed(${idx})"
|
||
style="width:18px;height:18px;cursor:pointer;">
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p>Is this information correct?</p>
|
||
<div class="action-buttons">
|
||
<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
|
||
const hoursSchedule = [];
|
||
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||
|
||
for (let i = 0; i < 7; i++) {
|
||
const openTime = document.getElementById(`open_${i}`).value;
|
||
const closeTime = document.getElementById(`close_${i}`).value;
|
||
const isClosed = document.getElementById(`closed_${i}`).checked;
|
||
|
||
hoursSchedule.push({
|
||
day: dayNames[i],
|
||
dayId: i + 1, // 1=Monday, 7=Sunday
|
||
open: isClosed ? openTime : openTime, // If closed, both times will be the same
|
||
close: isClosed ? openTime : closeTime // Set close = open when closed
|
||
});
|
||
}
|
||
|
||
// Update stored data with any edits
|
||
config.extractedData.business = {
|
||
name: document.getElementById('bizName').value,
|
||
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 sourceImg = mod.sourceImageIndex ? `Image ${mod.sourceImageIndex}` : 'Unknown source';
|
||
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">
|
||
<span class="source-badge">${sourceImg}</span>
|
||
${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 items
|
||
showItemsStep();
|
||
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();
|
||
showItemsStep();
|
||
return;
|
||
}
|
||
|
||
const modifier = uncertainModifiers[currentIndex];
|
||
|
||
// 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>
|
||
<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?bid=${finalBusinessId}#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>
|