2532 lines
89 KiB
HTML
2532 lines
89 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" style="flex-direction: column; align-items: stretch; gap: 8px;">
|
||
<span class="summary-stat-label">Menu Name</span>
|
||
<input type="text" id="menuNameInput" value="Main Menu" placeholder="e.g., Main Menu, Lunch, Dinner"
|
||
style="padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 14px;">
|
||
</div>
|
||
<div class="summary-stat" style="flex-direction: column; align-items: stretch; gap: 8px;">
|
||
<span class="summary-stat-label">Menu Hours</span>
|
||
<div style="display: flex; gap: 12px; align-items: center;">
|
||
<input type="time" id="menuStartTime" style="padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 14px; flex: 1;">
|
||
<span style="color: var(--gray-500);">to</span>
|
||
<input type="time" id="menuEndTime" style="padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 14px; flex: 1;">
|
||
</div>
|
||
<small style="color: var(--gray-500);">Leave empty for all-day availability. You can create additional menus later in the Menu Builder.</small>
|
||
</div>
|
||
<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>
|
||
|
||
<!-- Community Meal Participation -->
|
||
<div class="summary-card" id="communityMealCard" style="margin-top: 16px;">
|
||
<div class="summary-card-header">
|
||
<h3>Community Meal Participation</h3>
|
||
<p style="margin: 4px 0 0; color: var(--gray-500); font-size: 14px;">Choose how this location participates.</p>
|
||
</div>
|
||
<div class="summary-card-body" style="flex-direction: column; gap: 12px;">
|
||
<label style="display: flex; align-items: flex-start; gap: 12px; cursor: pointer; padding: 12px; border: 2px solid var(--primary); border-radius: 8px; background: rgba(99, 102, 241, 0.05);" id="communityMealOption1">
|
||
<input type="radio" name="communityMealType" value="1" checked
|
||
onchange="updateCommunityMealSelection()"
|
||
style="margin-top: 3px; width: 18px; height: 18px; accent-color: var(--primary); flex-shrink: 0;">
|
||
<div>
|
||
<strong style="display: block; color: var(--gray-900);">Provide Community Meals</strong>
|
||
<span style="color: var(--gray-500); font-size: 13px;">Offer one Community Meal per service window and receive a reduced Payfrit fee.</span>
|
||
</div>
|
||
</label>
|
||
<label style="display: flex; align-items: flex-start; gap: 12px; cursor: pointer; padding: 12px; border: 2px solid var(--gray-200); border-radius: 8px;" id="communityMealOption2">
|
||
<input type="radio" name="communityMealType" value="2"
|
||
onchange="updateCommunityMealSelection()"
|
||
style="margin-top: 3px; width: 18px; height: 18px; accent-color: var(--primary); flex-shrink: 0;">
|
||
<div>
|
||
<strong style="display: block; color: var(--gray-900);">Support a local food bank instead</strong>
|
||
<span style="color: var(--gray-500); font-size: 13px;">A combined 1.0% contribution (business + guest) is donated locally.</span>
|
||
</div>
|
||
</label>
|
||
<a href="/uploads/docs/Payfrit_Community_Meal_Participation_One_Pager.pdf" target="_blank"
|
||
style="display: inline-flex; align-items: center; gap: 6px; color: var(--primary); font-size: 13px; text-decoration: none; margin-top: 4px;">
|
||
<svg width="14" height="14" 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>
|
||
Learn more about Community Meal Participation
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-buttons">
|
||
<button class="btn btn-outline" onclick="startOver()">Start Over</button>
|
||
<button class="btn btn-success" onclick="saveMenu()">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||
<polyline points="17 21 17 13 7 13 7 21"/>
|
||
<polyline points="7 3 7 8 15 8"/>
|
||
</svg>
|
||
Save Menu
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Toast Container -->
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
|
||
<!-- Image Preview Modal -->
|
||
<div id="imagePreviewModal" class="image-modal" onclick="closeImagePreview(event)">
|
||
<div class="image-modal-content">
|
||
<span class="image-modal-close" onclick="closeImagePreview()">×</span>
|
||
<img id="imagePreviewImg" src="" alt="Menu Image">
|
||
<div class="image-modal-caption" id="imagePreviewCaption"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.image-modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 9999;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.9);
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
.image-modal.active {
|
||
display: flex;
|
||
}
|
||
.image-modal-content {
|
||
position: relative;
|
||
max-width: 90%;
|
||
max-height: 90%;
|
||
}
|
||
.image-modal-content img {
|
||
max-width: 100%;
|
||
max-height: 85vh;
|
||
object-fit: contain;
|
||
border-radius: 8px;
|
||
}
|
||
.image-modal-close {
|
||
position: absolute;
|
||
top: -40px;
|
||
right: 0;
|
||
color: white;
|
||
font-size: 32px;
|
||
cursor: pointer;
|
||
padding: 5px 10px;
|
||
}
|
||
.image-modal-close:hover {
|
||
color: #ccc;
|
||
}
|
||
.image-modal-caption {
|
||
color: white;
|
||
text-align: center;
|
||
padding: 10px;
|
||
font-size: 14px;
|
||
}
|
||
.source-badge.clickable {
|
||
cursor: pointer;
|
||
text-decoration: underline;
|
||
color: var(--primary);
|
||
}
|
||
.source-badge.clickable:hover {
|
||
color: var(--primary-hover);
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
// US States (from tt_States)
|
||
const US_STATES = [
|
||
{abbr:'AL',name:'Alabama'},{abbr:'AK',name:'Alaska'},{abbr:'AZ',name:'Arizona'},{abbr:'AR',name:'Arkansas'},
|
||
{abbr:'CA',name:'California'},{abbr:'CO',name:'Colorado'},{abbr:'CT',name:'Connecticut'},{abbr:'DE',name:'Delaware'},
|
||
{abbr:'DC',name:'Dist. of Columbia'},{abbr:'FL',name:'Florida'},{abbr:'GA',name:'Georgia'},{abbr:'HI',name:'Hawaii'},
|
||
{abbr:'ID',name:'Idaho'},{abbr:'IL',name:'Illinois'},{abbr:'IN',name:'Indiana'},{abbr:'IA',name:'Iowa'},
|
||
{abbr:'KS',name:'Kansas'},{abbr:'KY',name:'Kentucky'},{abbr:'LA',name:'Louisiana'},{abbr:'ME',name:'Maine'},
|
||
{abbr:'MD',name:'Maryland'},{abbr:'MA',name:'Massachusetts'},{abbr:'MI',name:'Michigan'},{abbr:'MN',name:'Minnesota'},
|
||
{abbr:'MS',name:'Mississippi'},{abbr:'MO',name:'Missouri'},{abbr:'MT',name:'Montana'},{abbr:'NE',name:'Nebraska'},
|
||
{abbr:'NV',name:'Nevada'},{abbr:'NH',name:'New Hampshire'},{abbr:'NJ',name:'New Jersey'},{abbr:'NM',name:'New Mexico'},
|
||
{abbr:'NY',name:'New York'},{abbr:'NC',name:'North Carolina'},{abbr:'ND',name:'North Dakota'},{abbr:'OH',name:'Ohio'},
|
||
{abbr:'OK',name:'Oklahoma'},{abbr:'OR',name:'Oregon'},{abbr:'PA',name:'Pennsylvania'},{abbr:'RI',name:'Rhode Island'},
|
||
{abbr:'SC',name:'South Carolina'},{abbr:'SD',name:'South Dakota'},{abbr:'TN',name:'Tennessee'},{abbr:'TX',name:'Texas'},
|
||
{abbr:'UT',name:'Utah'},{abbr:'VT',name:'Vermont'},{abbr:'VA',name:'Virginia'},{abbr:'WA',name:'Washington'},
|
||
{abbr:'WV',name:'West Virginia'},{abbr:'WI',name:'Wisconsin'},{abbr:'WY',name:'Wyoming'}
|
||
];
|
||
|
||
function buildStateOptions(selectedAbbr) {
|
||
const upper = (selectedAbbr || '').toUpperCase();
|
||
return '<option value="">Select...</option>' +
|
||
US_STATES.map(s => `<option value="${s.abbr}"${s.abbr === upper ? ' selected' : ''}>${s.abbr} - ${s.name}</option>`).join('');
|
||
}
|
||
|
||
// Configuration
|
||
const config = {
|
||
businessId: null,
|
||
apiBaseUrl: '',
|
||
uploadedFiles: [],
|
||
extractedData: {
|
||
business: {},
|
||
categories: [],
|
||
modifiers: [],
|
||
items: []
|
||
},
|
||
currentStep: 1,
|
||
imageObjectUrls: [] // Store object URLs for uploaded images
|
||
};
|
||
|
||
// Image preview functions
|
||
function showImagePreview(imageIndex) {
|
||
// imageIndex is 1-based from the API
|
||
const fileIndex = imageIndex - 1;
|
||
if (fileIndex < 0 || fileIndex >= config.uploadedFiles.length) {
|
||
console.error('Invalid image index:', imageIndex);
|
||
return;
|
||
}
|
||
|
||
const file = config.uploadedFiles[fileIndex];
|
||
|
||
// Create object URL if not cached
|
||
if (!config.imageObjectUrls[fileIndex]) {
|
||
config.imageObjectUrls[fileIndex] = URL.createObjectURL(file);
|
||
}
|
||
|
||
const modal = document.getElementById('imagePreviewModal');
|
||
const img = document.getElementById('imagePreviewImg');
|
||
const caption = document.getElementById('imagePreviewCaption');
|
||
|
||
img.src = config.imageObjectUrls[fileIndex];
|
||
caption.textContent = `Image ${imageIndex}: ${file.name}`;
|
||
modal.classList.add('active');
|
||
|
||
// Prevent body scroll
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
function closeImagePreview(event) {
|
||
// If event is passed, only close if clicking the background (not the image)
|
||
if (event && event.target.id !== 'imagePreviewModal') {
|
||
return;
|
||
}
|
||
const modal = document.getElementById('imagePreviewModal');
|
||
modal.classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
// Close on escape key
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
closeImagePreview();
|
||
}
|
||
});
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initializeConfig();
|
||
setupUploadZone();
|
||
loadBusinessInfo();
|
||
|
||
// In add-menu mode, update the header to show the menu name
|
||
if (config.menuId && config.menuName) {
|
||
document.querySelector('.wizard-header h1').textContent = `Setup: ${config.menuName}`;
|
||
document.querySelector('.wizard-header p').textContent = 'Upload menu images or PDFs to extract categories, items, and modifiers for this menu.';
|
||
}
|
||
});
|
||
|
||
function initializeConfig() {
|
||
// Check for add-menu context from sessionStorage
|
||
const wizardMenu = sessionStorage.getItem('payfrit_wizard_menu');
|
||
if (wizardMenu) {
|
||
try {
|
||
const ctx = JSON.parse(wizardMenu);
|
||
config.businessId = ctx.businessId || null;
|
||
config.menuId = ctx.menuId || null;
|
||
config.menuName = ctx.menuName || null;
|
||
sessionStorage.removeItem('payfrit_wizard_menu');
|
||
} catch (e) {
|
||
sessionStorage.removeItem('payfrit_wizard_menu');
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
|
||
console.log('Wizard initialized. BusinessId:', config.businessId, 'MenuId:', config.menuId, '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 = '';
|
||
|
||
// In add-menu mode, skip business info and header — go straight to categories
|
||
if (config.businessId && config.menuId) {
|
||
showCategoriesStep();
|
||
} else {
|
||
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
|
||
};
|
||
const dayNames = Object.keys(dayMap);
|
||
|
||
// Initialize all days as open with defaults
|
||
for (let i = 0; i < 7; i++) {
|
||
schedule.push({ open: '09:00', close: '17:00', closed: false });
|
||
}
|
||
|
||
if (!hoursText || !hoursText.trim()) {
|
||
return schedule;
|
||
}
|
||
|
||
const convertTo24Hour = (hour, minute, ampm) => {
|
||
let h = parseInt(hour);
|
||
const m = minute ? parseInt(minute) : 0;
|
||
if (ampm) {
|
||
ampm = ampm.replace(/\./g, '').toLowerCase();
|
||
if (ampm === 'pm' && h < 12) h += 12;
|
||
if (ampm === 'am' && h === 12) h = 0;
|
||
}
|
||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||
};
|
||
|
||
const 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;
|
||
// Day name pattern (matches day abbreviations/names)
|
||
const dayPattern = new RegExp('(' + dayNames.join('|') + ')', 'gi');
|
||
|
||
// Split by comma, semicolon, or newline to get individual segments
|
||
const segments = hoursText.split(/[,;\n]+/).map(s => s.trim()).filter(s => s.length);
|
||
|
||
let appliedAny = false;
|
||
|
||
for (const segment of segments) {
|
||
// Find time range in this segment
|
||
const timeMatch = segment.match(timePattern);
|
||
if (!timeMatch) continue;
|
||
|
||
const openTime = convertTo24Hour(timeMatch[1], timeMatch[2], timeMatch[3]);
|
||
const closeTime = convertTo24Hour(timeMatch[4], timeMatch[5], timeMatch[6]);
|
||
|
||
// Find all day names in this segment
|
||
const foundDays = [];
|
||
let dayMatch;
|
||
dayPattern.lastIndex = 0;
|
||
while ((dayMatch = dayPattern.exec(segment)) !== null) {
|
||
foundDays.push(dayMap[dayMatch[1].toLowerCase()]);
|
||
}
|
||
|
||
if (foundDays.length === 0) {
|
||
// No days specified - apply to all days
|
||
for (let i = 0; i < 7; i++) {
|
||
schedule[i] = { open: openTime, close: closeTime, closed: false };
|
||
}
|
||
appliedAny = true;
|
||
} else if (foundDays.length >= 2) {
|
||
// Check for range (e.g. "Mon-Thu" or "Mon, Tue, Wed, Thur")
|
||
// Look for dash/en-dash between two day names indicating a range
|
||
const rangeMatch = segment.match(new RegExp('(' + dayNames.join('|') + ')\\s*[-–]\\s*(' + dayNames.join('|') + ')', 'i'));
|
||
if (rangeMatch) {
|
||
// Day range like "Mon-Thu"
|
||
const startDay = dayMap[rangeMatch[1].toLowerCase()];
|
||
const endDay = dayMap[rangeMatch[2].toLowerCase()];
|
||
let d = startDay;
|
||
while (true) {
|
||
schedule[d] = { open: openTime, close: closeTime, closed: false };
|
||
if (d === endDay) break;
|
||
d = (d + 1) % 7;
|
||
}
|
||
} else {
|
||
// Comma-separated days like "Mon, Tue, Wed, Thur"
|
||
for (const d of foundDays) {
|
||
schedule[d] = { open: openTime, close: closeTime, closed: false };
|
||
}
|
||
}
|
||
appliedAny = true;
|
||
} else {
|
||
// Single day
|
||
schedule[foundDays[0]] = { open: openTime, close: closeTime, closed: false };
|
||
appliedAny = true;
|
||
}
|
||
}
|
||
|
||
// Fallback: if no segments matched, try the whole string as one time
|
||
if (!appliedAny) {
|
||
const timeMatch = hoursText.match(timePattern);
|
||
if (timeMatch) {
|
||
const openTime = convertTo24Hour(timeMatch[1], timeMatch[2], timeMatch[3]);
|
||
const closeTime = convertTo24Hour(timeMatch[4], timeMatch[5], timeMatch[6]);
|
||
for (let i = 0; i < 7; i++) {
|
||
schedule[i] = { open: openTime, close: closeTime, closed: false };
|
||
}
|
||
}
|
||
}
|
||
|
||
return schedule;
|
||
}
|
||
|
||
// Toggle day closed/open - time inputs always stay editable
|
||
function toggleDayClosed(dayIdx) {
|
||
// No-op: time inputs stay editable so user can set hours before unchecking "Closed"
|
||
}
|
||
|
||
// Check for duplicate businesses before creating a new one
|
||
async function checkForDuplicateBusiness(biz) {
|
||
try {
|
||
const response = await fetch(`${config.apiBaseUrl}/setup/checkDuplicate.cfm`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: biz.name || '',
|
||
addressLine1: biz.addressLine1 || '',
|
||
city: biz.city || '',
|
||
state: biz.state || '',
|
||
zip: biz.zip || ''
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.OK && result.duplicates && result.duplicates.length > 0) {
|
||
// Show duplicate warning
|
||
const dupList = result.duplicates.map(d =>
|
||
`<li><strong>${d.BusinessName}</strong> (ID: ${d.BusinessID})<br><small>${d.Address || 'No address'}</small></li>`
|
||
).join('');
|
||
|
||
addMessage('ai', `
|
||
<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
||
<p style="margin: 0 0 12px 0; font-weight: 600; color: #92400e;">
|
||
Possible duplicate found!
|
||
</p>
|
||
<p style="margin: 0 0 12px 0; color: #78350f;">
|
||
A business with a similar name or address already exists:
|
||
</p>
|
||
<ul style="margin: 0 0 16px 0; padding-left: 20px; color: #78350f;">
|
||
${dupList}
|
||
</ul>
|
||
<p style="margin: 0; color: #78350f; font-size: 14px;">
|
||
You can continue to create a new business, or go back and select the existing one from the portal.
|
||
</p>
|
||
</div>
|
||
`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error checking for duplicates:', error);
|
||
// Don't block the wizard if duplicate check fails
|
||
}
|
||
}
|
||
|
||
// Step 1: Business Info
|
||
async function showBusinessInfoStep() {
|
||
updateProgress(2);
|
||
const biz = config.extractedData.business || {};
|
||
|
||
// Check for duplicate businesses before showing the form
|
||
await checkForDuplicateBusiness(biz);
|
||
|
||
console.log('Business data:', biz);
|
||
|
||
// Parse address into components if it's a single string
|
||
let addressLine1 = biz.addressLine1 || '';
|
||
let city = biz.city || '';
|
||
let state = biz.state || '';
|
||
let zip = biz.zip || '';
|
||
|
||
if (biz.address && !addressLine1) {
|
||
console.log('Parsing address:', biz.address);
|
||
|
||
// Parse from the end forward: ZIP (5 digits), then State (2 letters), then City + AddressLine1
|
||
let remaining = biz.address.trim();
|
||
|
||
// Extract ZIP (last 5 digits, optionally with -4)
|
||
const zipMatch = remaining.match(/\b(\d{5})(?:-\d{4})?\s*$/);
|
||
if (zipMatch) {
|
||
zip = zipMatch[1];
|
||
remaining = remaining.substring(0, zipMatch.index).trim();
|
||
console.log('Found ZIP:', zip, 'Remaining:', remaining);
|
||
}
|
||
|
||
// Extract State (2 letters before ZIP)
|
||
const stateMatch = remaining.match(/\b([A-Z]{2})\s*$/i);
|
||
if (stateMatch) {
|
||
state = stateMatch[1].toUpperCase();
|
||
remaining = remaining.substring(0, stateMatch.index).trim();
|
||
console.log('Found State:', state, 'Remaining:', remaining);
|
||
}
|
||
|
||
// What's left is AddressLine1 + City
|
||
// Try to split by comma first
|
||
if (remaining.includes(',')) {
|
||
const parts = remaining.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
||
addressLine1 = parts[0] || '';
|
||
city = parts.slice(1).join(', ');
|
||
} else {
|
||
// No comma - try to find last sequence of words as city
|
||
// Cities are usually 1-3 words at the end
|
||
const words = remaining.split(/\s+/);
|
||
if (words.length > 3) {
|
||
// Assume last 2 words are city, rest is address
|
||
city = words.slice(-2).join(' ');
|
||
addressLine1 = words.slice(0, -2).join(' ');
|
||
} else if (words.length > 1) {
|
||
// Assume last word is city
|
||
city = words[words.length - 1];
|
||
addressLine1 = words.slice(0, -1).join(' ');
|
||
} else {
|
||
// All in address line 1
|
||
addressLine1 = remaining;
|
||
}
|
||
}
|
||
|
||
// Clean up any trailing commas, periods, or whitespace from city
|
||
city = city.replace(/[,.\s]+$/, '').trim();
|
||
}
|
||
|
||
console.log('Parsed address:', { addressLine1, city, state, zip });
|
||
|
||
// Parse hours into a 7-day schedule
|
||
const hoursSchedule = parseHoursString(biz.hours || '');
|
||
|
||
addMessage('ai', `
|
||
<p>I found your restaurant information:</p>
|
||
<div class="extracted-value editable">
|
||
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Restaurant Name</label>
|
||
<input type="text" id="bizName" value="${biz.name || ''}" placeholder="Restaurant name" required>
|
||
</div>
|
||
<div class="extracted-value editable">
|
||
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Address Line 1</label>
|
||
<input type="text" id="bizAddressLine1" value="${addressLine1}" placeholder="123 Main St">
|
||
</div>
|
||
<div class="extracted-value editable">
|
||
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">City</label>
|
||
<input type="text" id="bizCity" value="${city}" placeholder="Los Angeles">
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||
<div class="extracted-value editable">
|
||
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">State</label>
|
||
<select id="bizState" style="padding:8px 12px;border:1px solid var(--gray-300);border-radius:6px;font-size:14px;width:100%;background:white;">
|
||
${buildStateOptions(state)}
|
||
</select>
|
||
</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;">Sales Tax Rate (%)</label>
|
||
<input type="number" id="bizTaxRate" value="" placeholder="8.25" step="0.01" min="0" max="25" style="width:120px;">
|
||
<span style="font-size:11px;color:var(--gray-400);margin-left:8px;">e.g. 8.25 for 8.25%</span>
|
||
</div>
|
||
<div class="extracted-value editable">
|
||
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Business Hours</label>
|
||
<table style="width:100%;border-collapse:collapse;margin-top:8px;">
|
||
<thead>
|
||
<tr style="border-bottom:2px solid var(--gray-300);">
|
||
<th style="text-align:left;padding:8px;font-size:12px;color:var(--gray-600);">Day</th>
|
||
<th style="text-align:left;padding:8px;font-size:12px;color:var(--gray-600);">Open</th>
|
||
<th style="text-align:left;padding:8px;font-size:12px;color:var(--gray-600);">Close</th>
|
||
<th style="text-align:center;padding:8px;font-size:12px;color:var(--gray-600);">Closed</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'].map((day, idx) => {
|
||
const dayData = hoursSchedule[idx];
|
||
return `
|
||
<tr style="border-bottom:1px solid var(--gray-200);">
|
||
<td style="padding:8px;font-weight:500;">${day}</td>
|
||
<td style="padding:8px;">
|
||
<input type="time" id="open_${idx}" value="${dayData.open}"
|
||
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;">
|
||
</td>
|
||
<td style="padding:8px;">
|
||
<input type="time" id="close_${idx}" value="${dayData.close}"
|
||
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;">
|
||
</td>
|
||
<td style="padding:8px;text-align:center;">
|
||
<input type="checkbox" id="closed_${idx}" ${dayData.closed ? 'checked' : ''}
|
||
onchange="toggleDayClosed(${idx})"
|
||
style="width:18px;height:18px;cursor:pointer;">
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p>Is this information correct?</p>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-success" onclick="confirmBusinessInfo()">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="20 6 9 17 4 12"/>
|
||
</svg>
|
||
Looks Good
|
||
</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
function confirmBusinessInfo() {
|
||
// Collect hours from the table - only include non-closed days
|
||
const hoursSchedule = [];
|
||
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||
|
||
for (let i = 0; i < 7; i++) {
|
||
const openTime = document.getElementById(`open_${i}`).value;
|
||
const closeTime = document.getElementById(`close_${i}`).value;
|
||
const isClosed = document.getElementById(`closed_${i}`).checked;
|
||
|
||
// Only add days that are not closed
|
||
if (!isClosed) {
|
||
hoursSchedule.push({
|
||
day: dayNames[i],
|
||
dayId: i + 1, // 1=Monday, 7=Sunday
|
||
open: openTime,
|
||
close: closeTime
|
||
});
|
||
}
|
||
}
|
||
|
||
// Update stored data with any edits
|
||
config.extractedData.business = {
|
||
name: document.getElementById('bizName').value,
|
||
addressLine1: document.getElementById('bizAddressLine1').value,
|
||
city: document.getElementById('bizCity').value,
|
||
state: document.getElementById('bizState').value,
|
||
zip: document.getElementById('bizZip').value,
|
||
phone: document.getElementById('bizPhone').value,
|
||
taxRatePercent: parseFloat(document.getElementById('bizTaxRate').value) || 0,
|
||
hoursSchedule: hoursSchedule // Send the structured schedule instead of the raw hours string
|
||
};
|
||
|
||
// Move to header image step
|
||
showHeaderImageStep();
|
||
}
|
||
|
||
// Header Image step - between business info and categories
|
||
function showHeaderImageStep() {
|
||
addMessage('ai', `
|
||
<p><strong>Header Image</strong></p>
|
||
<p>This image appears at the top of your menu in the Payfrit app. It's the first thing customers see!</p>
|
||
<div style="background:var(--gray-100);border-radius:8px;padding:16px;margin:12px 0;">
|
||
<p style="font-weight:500;margin-bottom:8px;">Recommended specs:</p>
|
||
<ul style="margin:0;padding-left:20px;color:var(--gray-600);font-size:13px;">
|
||
<li><strong>Size:</strong> 1200 x 400 pixels</li>
|
||
<li><strong>Format:</strong> JPG or PNG</li>
|
||
<li><strong>Content:</strong> Your restaurant, food, or branding</li>
|
||
</ul>
|
||
</div>
|
||
<div id="headerUploadPreview" style="width:100%;height:120px;background:#333;border-radius:8px;margin:12px 0;background-size:cover;background-position:center;display:none;"></div>
|
||
<div style="display:flex;gap:12px;margin-top:12px;">
|
||
<label class="btn btn-secondary" style="cursor:pointer;flex:1;text-align:center;">
|
||
<input type="file" id="wizardHeaderFile" accept="image/png,image/jpeg,image/jpg" style="display:none;" onchange="previewWizardHeader(this)">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px;vertical-align:middle;">
|
||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
|
||
</svg>
|
||
Choose Image
|
||
</label>
|
||
</div>
|
||
<div class="action-buttons" style="margin-top:16px;">
|
||
<button class="btn btn-success" onclick="confirmHeaderImage()">
|
||
<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>
|
||
<button class="btn btn-secondary" onclick="skipHeaderImage()">Skip for Now</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
function previewWizardHeader(input) {
|
||
if (!input.files || !input.files[0]) return;
|
||
const file = input.files[0];
|
||
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
showToast('Image must be under 5MB', 'error');
|
||
input.value = '';
|
||
return;
|
||
}
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
const preview = document.getElementById('headerUploadPreview');
|
||
preview.style.backgroundImage = `url(${e.target.result})`;
|
||
preview.style.display = 'block';
|
||
};
|
||
reader.readAsDataURL(file);
|
||
|
||
// Store the file for upload after save
|
||
config.headerImageFile = file;
|
||
}
|
||
|
||
function confirmHeaderImage() {
|
||
showCategoriesStep();
|
||
}
|
||
|
||
function skipHeaderImage() {
|
||
config.headerImageFile = null;
|
||
showCategoriesStep();
|
||
}
|
||
|
||
// Step 2: Categories
|
||
function showCategoriesStep() {
|
||
updateProgress(3);
|
||
const categories = config.extractedData.categories || [];
|
||
|
||
let categoriesHtml = categories.map((cat, i) => `
|
||
<div class="extracted-list-item">
|
||
<input type="checkbox" checked data-index="${i}">
|
||
<span class="item-text">
|
||
<input type="text" value="${cat.name}" data-index="${i}">
|
||
</span>
|
||
<span class="item-count">${cat.itemCount || 0} items</span>
|
||
<button class="remove-item" onclick="removeCategory(${i})">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
`).join('');
|
||
|
||
addMessage('ai', `
|
||
<p>I found <strong>${categories.length} menu categories</strong>:</p>
|
||
<div class="extracted-list" id="categoriesList">
|
||
${categoriesHtml}
|
||
</div>
|
||
<div class="add-row">
|
||
<input type="text" id="newCategoryName" placeholder="Add new category...">
|
||
<button class="btn btn-secondary" onclick="addCategory()">Add</button>
|
||
</div>
|
||
<p>Uncheck any categories you don't want to include.</p>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-success" onclick="confirmCategories()">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="20 6 9 17 4 12"/>
|
||
</svg>
|
||
Continue
|
||
</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
function removeCategory(index) {
|
||
config.extractedData.categories.splice(index, 1);
|
||
// Rebuild the list
|
||
const list = document.getElementById('categoriesList');
|
||
const categories = config.extractedData.categories;
|
||
list.innerHTML = categories.map((cat, i) => `
|
||
<div class="extracted-list-item">
|
||
<input type="checkbox" checked data-index="${i}">
|
||
<span class="item-text">
|
||
<input type="text" value="${cat.name}" data-index="${i}">
|
||
</span>
|
||
<span class="item-count">${cat.itemCount || 0} items</span>
|
||
<button class="remove-item" onclick="removeCategory(${i})">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function addCategory() {
|
||
const input = document.getElementById('newCategoryName');
|
||
const name = input.value.trim();
|
||
if (!name) return;
|
||
|
||
config.extractedData.categories.push({ name, itemCount: 0 });
|
||
input.value = '';
|
||
|
||
// Rebuild list
|
||
removeCategory(-1); // Hacky way to rebuild without removing
|
||
config.extractedData.categories.pop(); // Undo the splice
|
||
|
||
const list = document.getElementById('categoriesList');
|
||
const categories = config.extractedData.categories;
|
||
list.innerHTML = categories.map((cat, i) => `
|
||
<div class="extracted-list-item">
|
||
<input type="checkbox" checked data-index="${i}">
|
||
<span class="item-text">
|
||
<input type="text" value="${cat.name}" data-index="${i}">
|
||
</span>
|
||
<span class="item-count">${cat.itemCount || 0} items</span>
|
||
<button class="remove-item" onclick="removeCategory(${i})">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function confirmCategories() {
|
||
// Update categories with any edits and filter unchecked
|
||
const list = document.getElementById('categoriesList');
|
||
const items = list.querySelectorAll('.extracted-list-item');
|
||
const updatedCategories = [];
|
||
|
||
items.forEach(item => {
|
||
const checkbox = item.querySelector('input[type="checkbox"]');
|
||
const nameInput = item.querySelector('.item-text input');
|
||
if (checkbox.checked) {
|
||
updatedCategories.push({
|
||
name: nameInput.value,
|
||
itemCount: config.extractedData.categories[checkbox.dataset.index]?.itemCount || 0
|
||
});
|
||
}
|
||
});
|
||
|
||
config.extractedData.categories = updatedCategories;
|
||
showModifiersStep();
|
||
}
|
||
|
||
// Step 3: Modifiers
|
||
function showModifiersStep() {
|
||
updateProgress(4);
|
||
const modifiers = config.extractedData.modifiers || [];
|
||
|
||
// Debug: Log the raw modifier data
|
||
console.log('=== MODIFIERS DEBUG ===');
|
||
console.log('Total modifiers:', modifiers.length);
|
||
modifiers.forEach((mod, i) => {
|
||
console.log(`Modifier ${i}:`, mod);
|
||
console.log(` - name:`, mod.name);
|
||
console.log(` - options type:`, typeof mod.options);
|
||
console.log(` - options:`, mod.options);
|
||
if (mod.options && mod.options.length > 0) {
|
||
mod.options.forEach((opt, j) => {
|
||
console.log(` Option ${j}:`, opt, `(type: ${typeof opt})`);
|
||
});
|
||
}
|
||
});
|
||
console.log('======================');
|
||
|
||
if (modifiers.length === 0) {
|
||
addMessage('ai', `
|
||
<p>I didn't detect any modifier templates (like size options, add-ons, etc.).</p>
|
||
<p>Would you like to add some manually, or continue without modifiers?</p>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-outline" onclick="showItemsStep()">Skip Modifiers</button>
|
||
<button class="btn btn-primary" onclick="showAddModifierForm()">Add Modifiers</button>
|
||
</div>
|
||
`);
|
||
return;
|
||
}
|
||
|
||
let modifiersHtml = modifiers.map((mod, i) => {
|
||
const sourceImgIndex = mod.sourceImageIndex;
|
||
const sourceImgBadge = sourceImgIndex
|
||
? `<span class="source-badge clickable" onclick="event.stopPropagation(); showImagePreview(${sourceImgIndex})">Image ${sourceImgIndex}</span>`
|
||
: '<span class="source-badge">Unknown source</span>';
|
||
const appliesToInfo = mod.appliesTo === 'category' && mod.categoryName
|
||
? `<span class="applies-to-badge">Applies to: ${mod.categoryName}</span>`
|
||
: mod.appliesTo === 'uncertain'
|
||
? '<span class="applies-to-badge uncertain">Uncertain application</span>'
|
||
: '';
|
||
|
||
const optionsCount = (mod.options || []).length;
|
||
const optionsList = (mod.options || []).filter(opt => opt && opt.name).map(opt => `
|
||
<div class="modifier-option-detail">
|
||
<span class="option-name">${opt.name}</span>
|
||
<span class="option-price">${opt.price ? `+$${opt.price.toFixed(2)}` : '$0.00'}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
return `
|
||
<div class="modifier-template" data-index="${i}">
|
||
<div class="modifier-header" onclick="toggleModifierDetails(${i})">
|
||
<input type="checkbox" checked data-index="${i}" onclick="event.stopPropagation()">
|
||
<div class="modifier-info">
|
||
<div class="modifier-name-row">
|
||
<span class="modifier-name">${mod.name || 'Unnamed'}</span>
|
||
<span class="modifier-type">${mod.required ? 'Required' : 'Optional'}</span>
|
||
</div>
|
||
<div class="modifier-meta">
|
||
${sourceImgBadge}
|
||
${appliesToInfo}
|
||
<span class="options-count">${optionsCount} option${optionsCount !== 1 ? 's' : ''}</span>
|
||
</div>
|
||
</div>
|
||
<span class="expand-icon">▼</span>
|
||
</div>
|
||
<div class="modifier-details" id="mod-details-${i}" style="display: none;">
|
||
<div class="modifier-options-list">
|
||
${optionsList}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
addMessage('ai', `
|
||
<p>I found <strong>${modifiers.length} modifier templates</strong> that can be applied to menu items:</p>
|
||
<div id="modifiersList">
|
||
${modifiersHtml}
|
||
</div>
|
||
<p>Uncheck any you don't want to use.</p>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-success" onclick="confirmModifiers()">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="20 6 9 17 4 12"/>
|
||
</svg>
|
||
Continue
|
||
</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
function toggleModifierDetails(index) {
|
||
const details = document.getElementById(`mod-details-${index}`);
|
||
const icon = details.previousElementSibling.querySelector('.expand-icon');
|
||
|
||
if (details.style.display === 'none') {
|
||
details.style.display = 'block';
|
||
icon.textContent = '▲';
|
||
} else {
|
||
details.style.display = 'none';
|
||
icon.textContent = '▼';
|
||
}
|
||
}
|
||
|
||
function confirmModifiers() {
|
||
const list = document.getElementById('modifiersList');
|
||
const templates = list.querySelectorAll('.modifier-template');
|
||
const updatedModifiers = [];
|
||
|
||
templates.forEach(template => {
|
||
const checkbox = template.querySelector('input[type="checkbox"]');
|
||
if (checkbox.checked) {
|
||
const index = parseInt(checkbox.dataset.index);
|
||
updatedModifiers.push(config.extractedData.modifiers[index]);
|
||
}
|
||
});
|
||
|
||
config.extractedData.modifiers = updatedModifiers;
|
||
|
||
// Go to items step (uncertain modifiers will be handled after items)
|
||
showItemsStep();
|
||
}
|
||
|
||
// New step: Handle uncertain modifier assignments
|
||
function showUncertainModifiersStep() {
|
||
const modifiers = config.extractedData.modifiers || [];
|
||
const categories = config.extractedData.categories || [];
|
||
|
||
// Find modifiers marked as "uncertain"
|
||
const uncertainModifiers = modifiers.filter(mod =>
|
||
mod.appliesTo === 'uncertain'
|
||
);
|
||
|
||
if (uncertainModifiers.length === 0 || categories.length === 0) {
|
||
// No uncertain modifiers or no categories, skip to final review
|
||
showFinalStep();
|
||
return;
|
||
}
|
||
|
||
// Initialize uncertain modifier assignment tracking
|
||
if (!config.uncertainModifierAssignments) {
|
||
config.uncertainModifierAssignments = {};
|
||
config.currentUncertainModIndex = 0;
|
||
}
|
||
|
||
const currentIndex = config.currentUncertainModIndex;
|
||
|
||
if (currentIndex >= uncertainModifiers.length) {
|
||
// All uncertain modifiers have been processed, apply assignments and continue
|
||
applyUncertainModifierAssignments();
|
||
showFinalStep();
|
||
return;
|
||
}
|
||
|
||
const modifier = uncertainModifiers[currentIndex];
|
||
|
||
// Build detailed modifier view
|
||
const sourceImgIndex = modifier.sourceImageIndex;
|
||
const sourceImgBadge = sourceImgIndex
|
||
? `<span class="source-badge clickable" onclick="showImagePreview(${sourceImgIndex})">Image ${sourceImgIndex}</span>`
|
||
: '<span class="source-badge">Unknown source</span>';
|
||
const optionsCount = (modifier.options || []).length;
|
||
const optionsList = (modifier.options || []).filter(opt => opt && opt.name).map(opt => `
|
||
<div class="modifier-option-detail">
|
||
<span class="option-name">${opt.name}</span>
|
||
<span class="option-price">${opt.price ? `+$${opt.price.toFixed(2)}` : '$0.00'}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Ask user about this modifier
|
||
const categoryOptions = categories.map((cat, i) => `
|
||
<label class="category-option">
|
||
<input type="checkbox" name="category-assign" value="${cat.name}">
|
||
<span>${cat.name}</span>
|
||
</label>
|
||
`).join('');
|
||
|
||
addMessage('ai', `
|
||
<p>I found the modifier template <strong>"${modifier.name}"</strong> but I'm not sure which items it applies to.</p>
|
||
<div class="modifier-details-view" style="background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 8px; padding: 16px; margin: 16px 0;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||
<span style="font-weight: 500;">${modifier.name}</span>
|
||
${sourceImgBadge}
|
||
</div>
|
||
<div style="color: var(--gray-600); font-size: 14px; margin-bottom: 8px;">
|
||
<strong>${optionsCount} option${optionsCount !== 1 ? 's' : ''}</strong> • ${modifier.required ? 'Required' : 'Optional'}
|
||
</div>
|
||
<div class="modifier-options-list">
|
||
${optionsList}
|
||
</div>
|
||
</div>
|
||
<p>Select the categories where this modifier should be applied, or skip if it doesn't apply automatically:</p>
|
||
<div class="category-selection" style="display: flex; flex-direction: column; gap: 8px; margin: 16px 0;">
|
||
${categoryOptions}
|
||
</div>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-outline" onclick="skipUncertainModifier()">Skip This Modifier</button>
|
||
<button class="btn btn-primary" onclick="assignUncertainModifier('${modifier.name}')">Apply to Selected Categories</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
function skipUncertainModifier() {
|
||
config.currentUncertainModIndex++;
|
||
showUncertainModifiersStep();
|
||
}
|
||
|
||
function assignUncertainModifier(modifierName) {
|
||
const checkboxes = document.querySelectorAll('input[name="category-assign"]:checked');
|
||
const selectedCategories = Array.from(checkboxes).map(cb => cb.value);
|
||
|
||
if (selectedCategories.length > 0) {
|
||
config.uncertainModifierAssignments[modifierName] = selectedCategories;
|
||
}
|
||
|
||
config.currentUncertainModIndex++;
|
||
showUncertainModifiersStep();
|
||
}
|
||
|
||
function applyUncertainModifierAssignments() {
|
||
// Apply the user's category selections to items
|
||
const items = config.extractedData.items || [];
|
||
const modifiers = config.extractedData.modifiers || [];
|
||
|
||
for (const [modifierName, categories] of Object.entries(config.uncertainModifierAssignments)) {
|
||
items.forEach(item => {
|
||
if (!item.modifiers) {
|
||
item.modifiers = [];
|
||
}
|
||
|
||
// If this item is in one of the selected categories, add the modifier
|
||
if (categories.includes(item.category)) {
|
||
if (!item.modifiers.includes(modifierName)) {
|
||
item.modifiers.push(modifierName);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update the modifier's appliesTo field to reflect the assignment
|
||
const modifier = modifiers.find(m => m.name === modifierName);
|
||
if (modifier) {
|
||
modifier.appliesTo = 'category';
|
||
modifier.categoryNames = categories; // Store all assigned categories
|
||
}
|
||
}
|
||
}
|
||
|
||
// Step 4: Items
|
||
function showItemsStep() {
|
||
updateProgress(5);
|
||
const items = config.extractedData.items || [];
|
||
const categories = config.extractedData.categories || [];
|
||
|
||
if (items.length === 0) {
|
||
addMessage('ai', `
|
||
<p>I couldn't extract any menu items. This might happen with complex or handwritten menus.</p>
|
||
<p>You can add items manually in the Menu Builder after completing this wizard.</p>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-success" onclick="showFinalStep()">Continue to Review</button>
|
||
</div>
|
||
`);
|
||
return;
|
||
}
|
||
|
||
// Group items by category
|
||
let itemsByCategory = {};
|
||
categories.forEach(cat => {
|
||
itemsByCategory[cat.name] = items.filter(item => item.category === cat.name);
|
||
});
|
||
|
||
let itemsHtml = '';
|
||
for (const [catName, catItems] of Object.entries(itemsByCategory)) {
|
||
if (catItems.length === 0) continue;
|
||
|
||
itemsHtml += `
|
||
<div style="margin-bottom: 16px;">
|
||
<h4 style="margin-bottom: 8px; color: var(--gray-700);">${catName} (${catItems.length})</h4>
|
||
<table class="items-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:40px;"></th>
|
||
<th>Item</th>
|
||
<th style="width:80px;">Price</th>
|
||
<th>Modifiers</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${catItems.map((item, i) => `
|
||
<tr>
|
||
<td><input type="checkbox" checked data-item-id="${item.id || i}"></td>
|
||
<td>
|
||
<strong>${item.name}</strong>
|
||
${item.description ? `<br><small style="color:var(--gray-500);">${item.description}</small>` : ''}
|
||
</td>
|
||
<td>$${parseFloat(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;
|
||
|
||
// In add-menu mode, hide menu name/hours/community meal (already set during menu creation)
|
||
if (config.menuId) {
|
||
document.getElementById('menuNameInput').parentElement.style.display = 'none';
|
||
document.getElementById('menuStartTime').closest('.summary-stat').style.display = 'none';
|
||
document.getElementById('communityMealCard').style.display = 'none';
|
||
} else {
|
||
// Set default menu hours based on business hours (earliest open, latest close)
|
||
const hoursSchedule = business.hoursSchedule || [];
|
||
if (hoursSchedule.length > 0) {
|
||
let earliestOpen = '23:59';
|
||
let latestClose = '00:00';
|
||
hoursSchedule.forEach(day => {
|
||
if (day.open && day.open < earliestOpen) earliestOpen = day.open;
|
||
if (day.close && day.close > latestClose) latestClose = day.close;
|
||
});
|
||
document.getElementById('menuStartTime').value = earliestOpen;
|
||
document.getElementById('menuEndTime').value = latestClose;
|
||
}
|
||
}
|
||
|
||
addMessage('ai', `
|
||
<p>Your menu is ready to save!</p>
|
||
${config.menuId && config.menuName
|
||
? `<p>Adding to: <strong>${config.menuName}</strong></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);
|
||
|
||
// In add-menu mode, skip menu name/hours/community meal — already set
|
||
if (!config.menuId) {
|
||
const menuName = document.getElementById('menuNameInput').value.trim() || 'Main Menu';
|
||
const menuStartTime = document.getElementById('menuStartTime')?.value || '';
|
||
const menuEndTime = document.getElementById('menuEndTime')?.value || '';
|
||
|
||
// Validate menu hours fall within business operating hours
|
||
if (menuStartTime && menuEndTime) {
|
||
const hoursSchedule = config.extractedData.business.hoursSchedule || [];
|
||
if (hoursSchedule.length > 0) {
|
||
let earliestOpen = '23:59';
|
||
let latestClose = '00:00';
|
||
hoursSchedule.forEach(day => {
|
||
if (day.open && day.open < earliestOpen) earliestOpen = day.open;
|
||
if (day.close && day.close > latestClose) latestClose = day.close;
|
||
});
|
||
|
||
if (menuStartTime < earliestOpen || menuEndTime > latestClose) {
|
||
showToast(`Menu hours must be within business operating hours (${earliestOpen} - ${latestClose})`, 'error');
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
config.extractedData.menuName = menuName;
|
||
config.extractedData.menuStartTime = menuStartTime;
|
||
config.extractedData.menuEndTime = menuEndTime;
|
||
|
||
// Community meal participation type (1=provide meals, 2=food bank)
|
||
const communityMealRadio = document.querySelector('input[name="communityMealType"]:checked');
|
||
config.extractedData.communityMealType = communityMealRadio ? parseInt(communityMealRadio.value) : 1;
|
||
}
|
||
|
||
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,
|
||
menuId: config.menuId || 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)
|
||
// Lucee serializes struct keys as uppercase, so check both cases
|
||
const summary = result.summary || result.SUMMARY || {};
|
||
const finalBusinessId = summary.businessId || summary.BUSINESSID || summary.businessid || config.businessId;
|
||
|
||
// Update localStorage with the new business ID to keep user logged in
|
||
localStorage.setItem('payfrit_portal_business', finalBusinessId);
|
||
|
||
// Upload header image if one was selected
|
||
if (config.headerImageFile && finalBusinessId) {
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('BusinessID', finalBusinessId);
|
||
formData.append('header', config.headerImageFile);
|
||
|
||
const headerResp = await fetch(`${config.apiBaseUrl}/menu/uploadHeader.cfm`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const headerResult = await headerResp.json();
|
||
if (headerResult.OK) {
|
||
console.log('Header image uploaded:', headerResult.HEADERURL);
|
||
} else {
|
||
console.error('Header upload failed:', headerResult.MESSAGE);
|
||
showToast('Menu saved, but header image upload failed. You can upload it later in Settings.', 'warning');
|
||
}
|
||
} catch (headerErr) {
|
||
console.error('Header upload error:', headerErr);
|
||
showToast('Menu saved, but header image upload failed. You can upload it later in Settings.', 'warning');
|
||
}
|
||
}
|
||
|
||
// Redirect back after a moment
|
||
setTimeout(() => {
|
||
window.location.href = config.menuId ? 'menu-builder.html' : 'index.html#menu';
|
||
}, 1500);
|
||
|
||
} catch (error) {
|
||
console.error('Save error:', error);
|
||
showToast('Failed to save: ' + error.message, 'error');
|
||
saveBtn.innerHTML = originalText;
|
||
saveBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function startOver() {
|
||
if (!confirm('Are you sure you want to start over? All extracted data will be lost.')) {
|
||
return;
|
||
}
|
||
|
||
config.uploadedFiles = [];
|
||
config.extractedData = { business: {}, categories: [], modifiers: [], items: [] };
|
||
config.currentStep = 1;
|
||
|
||
document.getElementById('conversation').innerHTML = '';
|
||
document.getElementById('filePreviewGrid').innerHTML = '';
|
||
document.getElementById('uploadSection').style.display = 'block';
|
||
document.getElementById('uploadZone').classList.remove('has-files');
|
||
document.getElementById('uploadActions').style.display = 'none';
|
||
document.getElementById('finalActions').classList.add('hidden');
|
||
document.getElementById('previewBtn').disabled = true;
|
||
|
||
updateProgress(1);
|
||
}
|
||
|
||
function openPreview() {
|
||
const { business, categories, modifiers, items } = config.extractedData;
|
||
|
||
// Build preview HTML
|
||
let previewHtml = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Menu Preview - ${business.name || 'Restaurant'}</title>
|
||
<style>
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
||
h1 { border-bottom: 2px solid #333; padding-bottom: 10px; }
|
||
h2 { color: #6366f1; margin-top: 24px; cursor: pointer; }
|
||
h2:hover { color: #4f46e5; }
|
||
.category { margin-bottom: 24px; }
|
||
.category-items { padding-left: 20px; }
|
||
.item { padding: 12px 0; border-bottom: 1px solid #eee; }
|
||
.item-header { display: flex; justify-content: space-between; }
|
||
.item-name { font-weight: 600; }
|
||
.item-price { color: #22c55e; font-weight: 600; }
|
||
.item-desc { color: #666; font-size: 14px; margin-top: 4px; }
|
||
.item-mods { margin-top: 8px; }
|
||
.mod-tag { display: inline-block; background: #e0e7ff; color: #4338ca; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-right: 4px; }
|
||
.business-info { background: #f3f4f6; padding: 16px; border-radius: 8px; margin-bottom: 24px; }
|
||
.collapsed .category-items { display: none; }
|
||
.toggle-icon { float: right; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>${business.name || 'Menu Preview'}</h1>
|
||
<div class="business-info">
|
||
${business.address ? `<div><strong>Address:</strong> ${business.address}</div>` : ''}
|
||
${business.phone ? `<div><strong>Phone:</strong> ${business.phone}</div>` : ''}
|
||
${business.hours ? `<div><strong>Hours:</strong> ${business.hours}</div>` : ''}
|
||
</div>
|
||
`;
|
||
|
||
categories.forEach(cat => {
|
||
const catItems = items.filter(item => item.category === cat.name);
|
||
previewHtml += `
|
||
<div class="category">
|
||
<h2 onclick="this.parentElement.classList.toggle('collapsed')">
|
||
${cat.name} <span class="toggle-icon">[+/-]</span>
|
||
</h2>
|
||
<div class="category-items">
|
||
${catItems.map(item => `
|
||
<div class="item">
|
||
<div class="item-header">
|
||
<span class="item-name">${item.name}</span>
|
||
<span class="item-price">$${(item.price || 0).toFixed(2)}</span>
|
||
</div>
|
||
${item.description ? `<div class="item-desc">${item.description}</div>` : ''}
|
||
${item.modifiers && item.modifiers.length > 0 ? `
|
||
<div class="item-mods">
|
||
${item.modifiers.map(m => `<span class="mod-tag">${m}</span>`).join('')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`).join('')}
|
||
${catItems.length === 0 ? '<div class="item" style="color:#999;">No items in this category</div>' : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
previewHtml += `
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
const previewWindow = window.open('', 'MenuPreview', 'width=900,height=700');
|
||
previewWindow.document.write(previewHtml);
|
||
previewWindow.document.close();
|
||
}
|
||
|
||
// Helper functions
|
||
function addMessage(type, content) {
|
||
const conversation = document.getElementById('conversation');
|
||
const avatar = type === 'ai'
|
||
? '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"/><path d="M12 6v6l4 2"/></svg>'
|
||
: 'U';
|
||
|
||
const message = document.createElement('div');
|
||
message.className = 'message';
|
||
message.innerHTML = `
|
||
<div class="message-avatar ${type}">${avatar}</div>
|
||
<div class="message-content">${content}</div>
|
||
`;
|
||
conversation.appendChild(message);
|
||
|
||
// Scroll to bottom
|
||
message.scrollIntoView({ behavior: 'smooth' });
|
||
}
|
||
|
||
function updateCommunityMealSelection() {
|
||
const selected = document.querySelector('input[name="communityMealType"]:checked').value;
|
||
const opt1 = document.getElementById('communityMealOption1');
|
||
const opt2 = document.getElementById('communityMealOption2');
|
||
if (selected === '1') {
|
||
opt1.style.borderColor = 'var(--primary)';
|
||
opt1.style.background = 'rgba(99, 102, 241, 0.05)';
|
||
opt2.style.borderColor = 'var(--gray-200)';
|
||
opt2.style.background = '';
|
||
} else {
|
||
opt2.style.borderColor = 'var(--primary)';
|
||
opt2.style.background = 'rgba(99, 102, 241, 0.05)';
|
||
opt1.style.borderColor = 'var(--gray-200)';
|
||
opt1.style.background = '';
|
||
}
|
||
}
|
||
|
||
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>
|