3986 lines
156 KiB
HTML
3986 lines
156 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;
|
||
}
|
||
|
||
.item-mod-groups { display: none; margin-top: 6px; }
|
||
.item-mod-groups.visible { display: block; }
|
||
.item-mod-group {
|
||
font-size: 12px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.item-mod-group-header {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
cursor: pointer;
|
||
color: var(--gray-600);
|
||
font-weight: 500;
|
||
}
|
||
.item-mod-group-header:hover { color: var(--gray-800); }
|
||
.mod-expand-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
background: var(--gray-200);
|
||
color: var(--gray-600);
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
flex-shrink: 0;
|
||
transition: transform 0.15s;
|
||
}
|
||
.mod-expand-btn.open { transform: rotate(45deg); }
|
||
.item-mod-options {
|
||
display: none;
|
||
margin: 2px 0 4px 20px;
|
||
padding-left: 8px;
|
||
border-left: 2px solid var(--gray-200);
|
||
}
|
||
.item-mod-options.visible { display: block; }
|
||
.item-mod-option {
|
||
font-size: 11px;
|
||
color: var(--gray-500);
|
||
padding: 1px 0;
|
||
}
|
||
.item-mod-option .opt-price { color: var(--gray-400); margin-left: 4px; }
|
||
|
||
/* 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="signup.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>Import from an existing source, or start from scratch</p>
|
||
</div>
|
||
|
||
<!-- Upload Section -->
|
||
<div id="uploadSection">
|
||
<!-- Import Method Tabs -->
|
||
<div class="import-tabs" style="display:flex;gap:0;margin-bottom:20px;border-radius:8px;overflow:hidden;border:1px solid var(--gray-300);">
|
||
<button class="import-tab active" id="tabUrl" onclick="switchImportTab('url')" style="flex:1;padding:12px 16px;border:none;background:var(--primary);color:white;font-weight:500;cursor:pointer;transition:all 0.2s;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;margin-right:6px;">
|
||
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||
</svg>
|
||
Import from URL
|
||
</button>
|
||
<button class="import-tab" id="tabUpload" onclick="switchImportTab('upload')" style="flex:1;padding:12px 16px;border:none;background:var(--gray-100);color:var(--gray-700);font-weight:500;cursor:pointer;transition:all 0.2s;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;margin-right:6px;">
|
||
<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>
|
||
Upload Files
|
||
</button>
|
||
<button class="import-tab" id="tabScratch" onclick="switchImportTab('scratch')" style="flex:1;padding:12px 16px;border:none;background:var(--gray-100);color:var(--gray-700);font-weight:500;cursor:pointer;transition:all 0.2s;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;margin-right:6px;">
|
||
<path d="M12 5v14M5 12h14"/>
|
||
</svg>
|
||
Start from Scratch
|
||
</button>
|
||
</div>
|
||
|
||
<!-- URL Import Panel -->
|
||
<div id="urlImportPanel">
|
||
<div style="background:var(--gray-50);border:2px dashed var(--gray-300);border-radius:12px;padding:32px;text-align:center;">
|
||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--gray-400)" stroke-width="1.5" style="margin-bottom:16px;">
|
||
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||
</svg>
|
||
<h3 style="margin:0 0 8px;color:var(--gray-700);">Enter Restaurant Website URL</h3>
|
||
<p style="margin:0 0 16px;color:var(--gray-500);font-size:14px;">We'll crawl the site to extract menu items, prices, images, and business info</p>
|
||
<input type="url" id="menuUrlInput" placeholder="https://restaurant-website.com" style="width:100%;max-width:400px;padding:12px 16px;border:1px solid var(--gray-300);border-radius:8px;font-size:16px;margin-bottom:16px;">
|
||
<div>
|
||
<button class="btn btn-primary" onclick="startUrlAnalysis()" style="min-width:160px;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px;">
|
||
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||
</svg>
|
||
Import Menu
|
||
</button>
|
||
</div>
|
||
<div style="margin:16px auto 0;max-width:440px;text-align:left;font-size:12px;color:var(--gray-400);">
|
||
<p style="margin:0 0 6px;font-weight:600;color:var(--gray-500);">Supported platforms:</p>
|
||
<p style="margin:0 0 2px;"><strong style="color:var(--gray-500);">Toast</strong> — full menu with modifiers & images (URL or saved page)</p>
|
||
<p style="margin:0 0 2px;"><strong style="color:var(--gray-500);">Grubhub</strong> — full menu with modifiers (URL only)</p>
|
||
<p style="margin:0 0 2px;"><strong style="color:var(--gray-500);">DoorDash / order.online</strong> — items, categories, prices (URL or saved page)</p>
|
||
<p style="margin:0 0 2px;"><strong style="color:var(--gray-500);">Uber Eats</strong> — full menu with modifiers (saved page)</p>
|
||
<p style="margin:0;"><strong style="color:var(--gray-500);">Other sites</strong> — upload saved page, images, or PDFs for AI extraction</p>
|
||
</div>
|
||
|
||
<!-- Divider -->
|
||
<div style="display:flex;align-items:center;margin:24px 0;gap:16px;">
|
||
<div style="flex:1;height:1px;background:var(--gray-300);"></div>
|
||
<span style="color:var(--gray-400);font-size:12px;text-transform:uppercase;">or upload saved page</span>
|
||
<div style="flex:1;height:1px;background:var(--gray-300);"></div>
|
||
</div>
|
||
|
||
<!-- Saved Page Upload -->
|
||
<p style="margin:0 0 8px;color:var(--gray-500);font-size:14px;">If the website blocks our crawler, save the menu page from your browser and upload it here</p>
|
||
<p style="margin:0 0 12px;color:#b45309;font-size:13px;font-weight:500;">Please disable ad blockers (uBlock Origin, AdBlock, etc.) before saving the page, or menu data may be missing.</p>
|
||
<input type="file" id="savedPageInput" accept=".html,.htm,.mhtml,.txt,.zip" style="display:none;" onchange="handleSavedPageUpload(event)">
|
||
<button class="btn btn-secondary" onclick="document.getElementById('savedPageInput').click()" style="min-width:180px;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px;">
|
||
<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"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>
|
||
</svg>
|
||
Upload Saved Page
|
||
</button>
|
||
<p style="margin:12px 0 0;color:var(--gray-400);font-size:12px;">HTML file or ZIP (Save Page As > Webpage, Complete)</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- File Upload Panel (hidden by default) -->
|
||
<div id="fileUploadPanel" style="display:none;">
|
||
<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>
|
||
|
||
<!-- Start from Scratch Panel (hidden by default) -->
|
||
<div id="scratchPanel" style="display:none;">
|
||
<div style="background:var(--gray-50);border:2px dashed var(--gray-300);border-radius:12px;padding:40px;text-align:center;">
|
||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--gray-400)" stroke-width="1.5" style="margin-bottom:16px;">
|
||
<path d="M12 5v14M5 12h14"/>
|
||
</svg>
|
||
<h3 style="margin:0 0 8px;color:var(--gray-700);">Build Your Menu from Scratch</h3>
|
||
<p style="margin:0 0 24px;color:var(--gray-500);font-size:14px;">Enter your business information and then use the Visual Menu Builder to add categories, items, and modifiers.</p>
|
||
<button class="btn btn-primary" onclick="startFromScratch()" style="min-width:200px;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px;">
|
||
<path d="M12 5v14M5 12h14"/>
|
||
</svg>
|
||
Get Started
|
||
</button>
|
||
</div>
|
||
</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 id="singleMenuSection">
|
||
<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>
|
||
<div id="multiMenuSection" style="display: none;">
|
||
<div class="summary-stat" style="flex-direction: column; align-items: stretch; gap: 8px;">
|
||
<span class="summary-stat-label">Detected Menus</span>
|
||
<div id="multiMenuList"></div>
|
||
<small style="color: var(--gray-500);">Each menu will be created separately with its own items and categories.</small>
|
||
</div>
|
||
</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" id="summarySubcategoriesRow" style="display:none;">
|
||
<span class="summary-stat-label" style="padding-left:12px;color:var(--gray-500);">Subcategories</span>
|
||
<span class="summary-stat-value" id="summarySubcategories">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 class="summary-stat" id="summaryImagesRow">
|
||
<span class="summary-stat-label">Images</span>
|
||
<span class="summary-stat-value" id="summaryImages" style="font-size: 13px; text-align: right;"></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
|
||
imageMappings: [], // For matching uploaded images to items (from HTML import)
|
||
itemImages: {}, // item ID -> File object for matched images
|
||
tempFolder: null // Temp folder ID from ZIP upload (for cleanup after save)
|
||
};
|
||
|
||
// 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 = 'signup.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';
|
||
}
|
||
}
|
||
|
||
// Switch between URL and file upload tabs
|
||
function switchImportTab(tab) {
|
||
const tabs = {
|
||
url: document.getElementById('tabUrl'),
|
||
upload: document.getElementById('tabUpload'),
|
||
scratch: document.getElementById('tabScratch')
|
||
};
|
||
const panels = {
|
||
url: document.getElementById('urlImportPanel'),
|
||
upload: document.getElementById('fileUploadPanel'),
|
||
scratch: document.getElementById('scratchPanel')
|
||
};
|
||
|
||
Object.keys(tabs).forEach(key => {
|
||
if (key === tab) {
|
||
tabs[key].style.background = 'var(--primary)';
|
||
tabs[key].style.color = 'white';
|
||
panels[key].style.display = 'block';
|
||
} else {
|
||
tabs[key].style.background = 'var(--gray-100)';
|
||
tabs[key].style.color = 'var(--gray-700)';
|
||
panels[key].style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Start from scratch — skip import, go straight to business info
|
||
function startFromScratch() {
|
||
config.scratchMode = true;
|
||
config.extractedData = {
|
||
business: {},
|
||
categories: [],
|
||
modifiers: [],
|
||
items: []
|
||
};
|
||
|
||
// Hide upload section, show conversation
|
||
document.getElementById('uploadSection').style.display = 'none';
|
||
|
||
showBusinessInfoStep();
|
||
}
|
||
|
||
// Save business only (scratch mode) then redirect to Menu Builder
|
||
async function saveScratchBusiness() {
|
||
// Add a default menu name and community meal type
|
||
config.extractedData.menuName = 'Main Menu';
|
||
config.extractedData.communityMealType = 1;
|
||
|
||
addMessage('ai', `
|
||
<p>Saving your business information...</p>
|
||
<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:16px auto;"></div>
|
||
`);
|
||
|
||
try {
|
||
const response = await fetch(`${config.apiBaseUrl}/setup/saveWizard.php`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
businessId: config.businessId || 0,
|
||
menuId: 0,
|
||
userId: config.userId,
|
||
data: config.extractedData,
|
||
tempFolder: null
|
||
})
|
||
});
|
||
|
||
const responseText = await response.text();
|
||
let result;
|
||
try {
|
||
result = JSON.parse(responseText);
|
||
} catch (e) {
|
||
throw new Error('Invalid response from server');
|
||
}
|
||
|
||
if (!result.OK) {
|
||
const errorMsg = result.errors && result.errors.length > 0
|
||
? result.errors.join('; ')
|
||
: (result.MESSAGE || 'Save failed');
|
||
throw new Error(errorMsg);
|
||
}
|
||
|
||
const summary = result.summary || result.SUMMARY || {};
|
||
const finalBusinessId = summary.businessId || summary.BUSINESSID || summary.businessid || config.businessId;
|
||
|
||
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.php`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const headerResult = await headerResp.json();
|
||
if (!headerResult.OK) {
|
||
console.error('Header upload failed:', headerResult.MESSAGE);
|
||
}
|
||
} catch (headerErr) {
|
||
console.error('Header upload error:', headerErr);
|
||
}
|
||
}
|
||
|
||
showToast('Business created! Redirecting to Menu Builder...', 'success');
|
||
|
||
setTimeout(() => {
|
||
window.location.href = '/portal/menu-builder.html';
|
||
}, 1500);
|
||
|
||
} catch (err) {
|
||
console.error('Save error:', err);
|
||
showToast('Error: ' + err.message, 'error');
|
||
// Remove the loading message and show retry
|
||
addMessage('ai', `
|
||
<p style="color:var(--error);">Something went wrong: ${err.message}</p>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-primary" onclick="saveScratchBusiness()">Try Again</button>
|
||
</div>
|
||
`);
|
||
}
|
||
}
|
||
|
||
// URL-based menu import
|
||
async function startUrlAnalysis() {
|
||
const urlInput = document.getElementById('menuUrlInput');
|
||
const url = urlInput.value.trim();
|
||
|
||
if (!url) {
|
||
showToast('Please enter a website URL', 'error');
|
||
return;
|
||
}
|
||
|
||
// Uber Eats URLs can't be scraped server-side — prompt user to save page
|
||
if (url.includes('ubereats.com/store') || url.includes('ubereats.com/store/')) {
|
||
document.getElementById('uploadSection').style.display = 'none';
|
||
addMessage('ai', `
|
||
<p><strong>Uber Eats pages can't be crawled directly</strong> (bot protection), but we can import from a saved page.</p>
|
||
<ol style="margin:12px 0;padding-left:20px;line-height:1.8;">
|
||
<li>Open the Uber Eats link in Chrome: <a href="${url}" target="_blank" style="word-break:break-all;">${url}</a></li>
|
||
<li>Wait for the menu to fully load (scroll down to load all items)</li>
|
||
<li>Press <strong>Ctrl+S</strong> to save the page as HTML</li>
|
||
<li>Upload the saved <code>.html</code> file below</li>
|
||
</ol>
|
||
<div style="margin-top:16px;">
|
||
<input type="file" id="uberEatsSavedPage" accept=".html,.htm" style="display:none;" onchange="handleSavedPageUpload(event)">
|
||
<button class="btn btn-primary" onclick="document.getElementById('uberEatsSavedPage').click()">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px;">
|
||
<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>
|
||
Upload Saved Page
|
||
</button>
|
||
<button class="btn btn-secondary" onclick="retryUrlAnalysis()" style="margin-left:8px;">Back</button>
|
||
</div>
|
||
`);
|
||
return;
|
||
}
|
||
|
||
// Hide upload section, show conversation
|
||
document.getElementById('uploadSection').style.display = 'none';
|
||
|
||
addMessage('ai', `
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<div class="loading-spinner"></div>
|
||
<span>Scanning website for menu pages...</span>
|
||
</div>
|
||
<p style="font-size:13px;color:var(--gray-500);margin-top:8px;">This takes about 30 seconds while I crawl the site and look for menu sub-pages.</p>
|
||
`);
|
||
|
||
try {
|
||
// Phase 1: Discovery — quick Playwright crawl to find menu pages
|
||
const discoverResp = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.php`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ mode: 'discover', url })
|
||
});
|
||
const discoverResult = await discoverResp.json();
|
||
|
||
if (!discoverResult.OK) {
|
||
throw new Error(discoverResult.MESSAGE || 'Failed to scan website');
|
||
}
|
||
|
||
console.log('=== DISCOVERY RESULT ===', discoverResult);
|
||
document.getElementById('conversation').innerHTML = '';
|
||
|
||
const menuPages = discoverResult.menuPages || [];
|
||
const siteName = discoverResult.siteName || '';
|
||
|
||
// Store business info and platform URLs from discovery
|
||
config.discoveredBusinessInfo = discoverResult.businessInfo || {};
|
||
config.platformPages = discoverResult.platformPages || [];
|
||
|
||
if (menuPages.length > 1) {
|
||
// Multiple menus found — show confirmation step
|
||
const pageListHtml = menuPages.map((p, i) => `
|
||
<label style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--gray-50);border-radius:8px;margin-bottom:6px;cursor:pointer;">
|
||
<input type="checkbox" checked data-menu-url="${p.url}" data-menu-name="${p.name}" style="width:18px;height:18px;">
|
||
<span style="font-weight:500;">${p.name}</span>
|
||
<span style="font-size:12px;color:var(--gray-400);margin-left:auto;">${p.url}</span>
|
||
</label>
|
||
`).join('');
|
||
|
||
addMessage('ai', `
|
||
<p>I found <strong>${menuPages.length} menu pages</strong>${siteName ? ' for <strong>' + siteName + '</strong>' : ''}:</p>
|
||
<div id="discoveredMenuPages" style="margin:12px 0;">
|
||
${pageListHtml}
|
||
</div>
|
||
<p style="font-size:13px;color:var(--gray-500);">Uncheck any that aren't actual menus. Each checked page will be analyzed separately.</p>
|
||
<div style="margin-top:8px;">
|
||
<label style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--gray-50);border-radius:8px;margin-bottom:6px;">
|
||
<span style="font-size:13px;">Additional menu URL (optional):</span>
|
||
<input type="text" id="extraMenuUrl" placeholder="https://..." style="flex:1;padding:4px 8px;border:1px solid var(--gray-200);border-radius:4px;font-size:13px;">
|
||
</label>
|
||
</div>
|
||
<div class="action-buttons" style="margin-top:12px;">
|
||
<button class="btn btn-primary" onclick="extractConfirmedMenuPages('${url}')">Extract Menus</button>
|
||
<button class="btn btn-secondary" onclick="skipDiscoveryExtractAll('${url}')">Extract as Single Page</button>
|
||
</div>
|
||
`);
|
||
} else {
|
||
// No sub-pages or just one — fall through to single-page extract
|
||
await extractSingleUrl(url);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('URL analysis error:', error);
|
||
document.getElementById('conversation').innerHTML = '';
|
||
addMessage('ai', `
|
||
<p>Sorry, I encountered an error importing from that URL:</p>
|
||
<p style="color: var(--danger);">${error.message}</p>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-primary" onclick="retryUrlAnalysis()">Try Again</button>
|
||
<button class="btn btn-secondary" onclick="switchToFileUpload()">Upload Files Instead</button>
|
||
</div>
|
||
`);
|
||
}
|
||
}
|
||
|
||
// Phase 2: Extract each confirmed menu page individually
|
||
async function extractConfirmedMenuPages(mainUrl) {
|
||
const checkboxes = document.querySelectorAll('#discoveredMenuPages input[type="checkbox"]:checked');
|
||
const pages = Array.from(checkboxes).map(cb => ({
|
||
url: cb.dataset.menuUrl,
|
||
name: cb.dataset.menuName
|
||
}));
|
||
|
||
// Add extra URL if provided
|
||
const extraUrl = (document.getElementById('extraMenuUrl')?.value || '').trim();
|
||
if (extraUrl) {
|
||
const slug = extraUrl.replace(/.*\//, '').replace(/[-_]/g, ' ');
|
||
pages.push({ url: extraUrl, name: slug ? slug.charAt(0).toUpperCase() + slug.slice(1) : 'Extra' });
|
||
}
|
||
|
||
if (pages.length === 0) {
|
||
alert('Please select at least one menu page.');
|
||
return;
|
||
}
|
||
|
||
document.getElementById('conversation').innerHTML = '';
|
||
|
||
// Progress container: completed results accumulate, spinner updates in place
|
||
addMessage('ai', '<div id="extractionProgress"></div><div id="extractionSpinner"></div>');
|
||
|
||
// Combined results — start with business info from discovery phase
|
||
const allItems = [];
|
||
const allCategories = [];
|
||
const allMenus = [];
|
||
let businessInfo = { ...(config.discoveredBusinessInfo || {}) };
|
||
let totalProcessed = 0;
|
||
|
||
for (const page of pages) {
|
||
document.getElementById('extractionSpinner').innerHTML = `
|
||
<div style="display:flex;align-items:center;gap:12px;margin-top:8px;">
|
||
<div class="loading-spinner"></div>
|
||
<span>Extracting <strong>${page.name}</strong> menu... (${totalProcessed + 1} of ${pages.length})</span>
|
||
</div>
|
||
`;
|
||
|
||
try {
|
||
const resp = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.php`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ mode: 'extract_page', url: page.url, menuName: page.name })
|
||
});
|
||
const result = await resp.json();
|
||
|
||
if (result.OK && result.DATA) {
|
||
const data = result.DATA;
|
||
// Merge business info — fill gaps from sub-page extractions
|
||
if (data.business) {
|
||
for (const [k, v] of Object.entries(data.business)) {
|
||
if (v && !businessInfo[k]) businessInfo[k] = v;
|
||
}
|
||
}
|
||
// Add categories with menu tag
|
||
(data.categories || []).forEach(cat => {
|
||
const catName = typeof cat === 'string' ? cat : cat.name;
|
||
if (catName && !allCategories.find(c => c.name === catName && c.menuName === page.name)) {
|
||
allCategories.push({ name: catName, menuName: page.name });
|
||
}
|
||
});
|
||
// Add items (already tagged with menu name by backend)
|
||
(data.items || []).forEach(item => allItems.push(item));
|
||
// Store menu with its schedule
|
||
const menuEntry = { name: page.name };
|
||
if (result.menuSchedule) menuEntry.schedule = result.menuSchedule;
|
||
allMenus.push(menuEntry);
|
||
|
||
totalProcessed++;
|
||
const scheduleNote = result.menuSchedule ? ` (${result.menuSchedule})` : '';
|
||
const line = document.createElement('div');
|
||
line.style.cssText = 'padding:2px 0;';
|
||
line.innerHTML = `<p style="margin:4px 0;">✓ Extracted <strong>${page.name}</strong>${scheduleNote}: ${data.items?.length || 0} items in ${data.categories?.length || 0} categories</p>`;
|
||
document.getElementById('extractionProgress').appendChild(line);
|
||
} else {
|
||
const line = document.createElement('div');
|
||
line.style.cssText = 'padding:2px 0;';
|
||
line.innerHTML = `<p style="margin:4px 0;color:var(--warning);">✗ Could not extract items from ${page.name} page.</p>`;
|
||
document.getElementById('extractionProgress').appendChild(line);
|
||
totalProcessed++;
|
||
}
|
||
} catch (err) {
|
||
console.error(`Error extracting ${page.name}:`, err);
|
||
const line = document.createElement('div');
|
||
line.className = 'message ai-message';
|
||
line.innerHTML = `<p style="margin:4px 0;color:var(--danger);">✗ Error extracting ${page.name}: ${err.message}</p>`;
|
||
progressDiv.appendChild(line);
|
||
totalProcessed++;
|
||
}
|
||
}
|
||
document.getElementById('extractionSpinner').remove();
|
||
|
||
// Compute max business hours from all menu schedules
|
||
if (allMenus.some(m => m.schedule) && !businessInfo.hours) {
|
||
businessInfo.hours = allMenus
|
||
.filter(m => m.schedule)
|
||
.map(m => `${m.name}: ${m.schedule}`)
|
||
.join('; ');
|
||
}
|
||
|
||
// Fetch images from ordering platform (stealth Playwright)
|
||
const platformUrl = (config.platformPages || [])[0] || '';
|
||
console.log('Platform pages from discovery:', config.platformPages);
|
||
console.log('Platform URL for images:', platformUrl);
|
||
console.log('Total items to match:', allItems.length);
|
||
console.log('Items with imageUrl already:', allItems.filter(i => i.imageUrl).length);
|
||
let platformImageMap = {};
|
||
if (platformUrl && allItems.length > 0) {
|
||
document.getElementById('conversation').innerHTML = '';
|
||
addMessage('ai', `
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<div class="loading-spinner"></div>
|
||
<span>Grabbing food photos from ordering platform...</span>
|
||
</div>
|
||
`);
|
||
|
||
try {
|
||
const imgResp = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.php`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ mode: 'platform_images', url: platformUrl, items: allItems.map(i => i.name) })
|
||
});
|
||
const imgResult = await imgResp.json();
|
||
console.log('Platform images raw response:', imgResult);
|
||
if (imgResult.OK && imgResult.imageMap) {
|
||
platformImageMap = imgResult.imageMap;
|
||
const matchCount = Object.keys(platformImageMap).length;
|
||
console.log('Image map keys (first 10):', Object.keys(platformImageMap).slice(0, 10));
|
||
console.log('Item names (first 10):', allItems.slice(0, 10).map(i => i.name));
|
||
// Match images to items by name
|
||
let matched = 0;
|
||
allItems.forEach(item => {
|
||
if (platformImageMap[item.name]) {
|
||
item.imageUrl = platformImageMap[item.name];
|
||
matched++;
|
||
}
|
||
});
|
||
console.log(`Platform images: ${matchCount} found, ${matched} matched to items`);
|
||
if (matchCount > 0 && matched === 0) {
|
||
console.log('MISMATCH DEBUG — first imageMap key:', JSON.stringify(Object.keys(platformImageMap)[0]));
|
||
console.log('MISMATCH DEBUG — first item name:', JSON.stringify(allItems[0]?.name));
|
||
}
|
||
} else {
|
||
console.log('Platform images: no imageMap in response', imgResult);
|
||
}
|
||
} catch (err) {
|
||
console.error('Platform image fetch error:', err);
|
||
}
|
||
}
|
||
|
||
// Build final combined data
|
||
document.getElementById('conversation').innerHTML = '';
|
||
config.extractedData = {
|
||
business: businessInfo,
|
||
categories: allCategories,
|
||
items: allItems,
|
||
modifiers: [],
|
||
menus: allMenus.length > 1 ? allMenus : [],
|
||
imageMappings: [],
|
||
imageUrls: [],
|
||
headerCandidateIndices: [],
|
||
};
|
||
config.sourceUrl = mainUrl;
|
||
config.imageMappings = [];
|
||
|
||
const totalItems = allItems.length;
|
||
const totalCats = allCategories.length;
|
||
const imgCount = allItems.filter(i => i.imageUrl).length;
|
||
const schedSummary = allMenus.filter(m => m.schedule).map(m => `${m.name}: ${m.schedule}`).join(', ');
|
||
addMessage('ai', `
|
||
<p>Done! Found <strong>${totalItems} items</strong> across <strong>${totalCats} categories</strong> in <strong>${allMenus.length} menus</strong>.</p>
|
||
${imgCount ? `<p style="font-size:13px;color:var(--gray-500);">${imgCount} item images found</p>` : ''}
|
||
${schedSummary ? `<p style="font-size:13px;color:var(--gray-500);">${schedSummary}</p>` : ''}
|
||
`);
|
||
|
||
console.log('=== MULTI-PAGE EXTRACT RESULT ===', config.extractedData);
|
||
|
||
// Brief pause then continue to business info
|
||
await new Promise(r => setTimeout(r, 1500));
|
||
document.getElementById('conversation').innerHTML = '';
|
||
if (config.businessId && config.menuId) {
|
||
showCategoriesStep();
|
||
} else {
|
||
showBusinessInfoStep();
|
||
}
|
||
}
|
||
|
||
// Fallback: extract entire site as single page (original behavior)
|
||
async function skipDiscoveryExtractAll(url) {
|
||
document.getElementById('conversation').innerHTML = '';
|
||
await extractSingleUrl(url);
|
||
}
|
||
|
||
async function extractSingleUrl(url) {
|
||
addMessage('ai', `
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<div class="loading-spinner"></div>
|
||
<span>Crawling website and extracting menu data...</span>
|
||
</div>
|
||
<p style="font-size:13px;color:var(--gray-500);margin-top:8px;">This may take 1-3 minutes while I crawl menu pages, grab photos from ordering platforms, and analyze everything.</p>
|
||
`);
|
||
|
||
try {
|
||
const response = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.php`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ url })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (!result.OK) {
|
||
throw new Error(result.MESSAGE || 'Failed to analyze URL');
|
||
}
|
||
|
||
config.extractedData = result.DATA;
|
||
config.sourceUrl = result.sourceUrl;
|
||
config.imageMappings = result.DATA.imageMappings || [];
|
||
|
||
console.log('=== URL IMPORT RESPONSE ===');
|
||
console.log('Source URL:', result.sourceUrl);
|
||
console.log('Extracted data:', result.DATA);
|
||
if (result.steps) console.log('Steps:', result.steps);
|
||
console.log('===========================');
|
||
|
||
document.getElementById('conversation').innerHTML = '';
|
||
|
||
if (config.businessId && config.menuId) {
|
||
showCategoriesStep();
|
||
} else {
|
||
showBusinessInfoStep();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('URL analysis error:', error);
|
||
document.getElementById('conversation').innerHTML = '';
|
||
addMessage('ai', `
|
||
<p>Sorry, I encountered an error importing from that URL:</p>
|
||
<p style="color: var(--danger);">${error.message}</p>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-primary" onclick="retryUrlAnalysis()">Try Again</button>
|
||
<button class="btn btn-secondary" onclick="switchToFileUpload()">Upload Files Instead</button>
|
||
</div>
|
||
`);
|
||
}
|
||
}
|
||
|
||
function retryUrlAnalysis() {
|
||
document.getElementById('conversation').innerHTML = '';
|
||
document.getElementById('uploadSection').style.display = 'block';
|
||
switchImportTab('url');
|
||
}
|
||
|
||
function switchToFileUpload() {
|
||
document.getElementById('conversation').innerHTML = '';
|
||
document.getElementById('uploadSection').style.display = 'block';
|
||
switchImportTab('upload');
|
||
}
|
||
|
||
// Handle uploaded saved page (HTML or ZIP)
|
||
async function handleSavedPageUpload(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
const isZip = file.name.toLowerCase().endsWith('.zip');
|
||
|
||
// Hide upload section, show conversation
|
||
document.getElementById('uploadSection').style.display = 'none';
|
||
|
||
addMessage('ai', `
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<div class="loading-spinner"></div>
|
||
<span>Analyzing saved page: ${file.name}...</span>
|
||
</div>
|
||
<p style="font-size:13px;color:var(--gray-500);margin-top:8px;">${isZip ? 'Uploading and extracting ZIP file...' : 'Extracting menu data from HTML content.'}</p>
|
||
`);
|
||
|
||
let result = null;
|
||
try {
|
||
if (isZip) {
|
||
// Upload ZIP file to server for extraction
|
||
const formData = new FormData();
|
||
formData.append('zipFile', file);
|
||
|
||
console.log('Uploading ZIP to:', `${config.apiBaseUrl}/setup/uploadSavedPage.php`);
|
||
const uploadResponse = await fetch(`${config.apiBaseUrl}/setup/uploadSavedPage.php`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
console.log('Upload response status:', uploadResponse.status, uploadResponse.statusText);
|
||
const uploadText = await uploadResponse.text();
|
||
console.log('Upload response text (first 500):', uploadText.substring(0, 500));
|
||
|
||
let uploadResult;
|
||
try {
|
||
uploadResult = JSON.parse(uploadText);
|
||
} catch (parseErr) {
|
||
console.error('Failed to parse upload response as JSON:', parseErr);
|
||
throw new Error('Server returned invalid response: ' + uploadText.substring(0, 200));
|
||
}
|
||
|
||
if (!uploadResult.OK) {
|
||
throw new Error(uploadResult.MESSAGE || 'Failed to upload ZIP file');
|
||
}
|
||
|
||
console.log('ZIP uploaded, extracted URL:', uploadResult.URL);
|
||
|
||
// Store temp folder ID for cleanup after wizard completes
|
||
if (uploadResult.FOLDER) {
|
||
config.tempFolder = uploadResult.FOLDER;
|
||
}
|
||
|
||
// Update loading message
|
||
document.getElementById('conversation').innerHTML = '';
|
||
addMessage('ai', `
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<div class="loading-spinner"></div>
|
||
<span>Scanning extracted page with browser...</span>
|
||
</div>
|
||
<p style="font-size:13px;color:var(--gray-500);margin-top:8px;">Using Playwright to render the saved page and extract menu data.</p>
|
||
`);
|
||
|
||
// Now analyze the extracted URL with Playwright
|
||
const response = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.php`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ url: uploadResult.URL })
|
||
});
|
||
|
||
const responseText = await response.text();
|
||
if (!responseText || responseText.trim().length === 0) {
|
||
throw new Error('Server returned empty response');
|
||
}
|
||
result = JSON.parse(responseText);
|
||
} else {
|
||
// Read HTML file content and send directly
|
||
const htmlContent = await file.text();
|
||
console.log('Sending HTML content, length:', htmlContent.length);
|
||
|
||
const response = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.php`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ html: htmlContent })
|
||
});
|
||
|
||
const responseText = await response.text();
|
||
console.log('Raw response (first 500 chars):', responseText.substring(0, 500));
|
||
|
||
if (!responseText || responseText.trim().length === 0) {
|
||
throw new Error('Server returned empty response');
|
||
}
|
||
|
||
try {
|
||
result = JSON.parse(responseText);
|
||
} catch (parseErr) {
|
||
console.error('JSON parse error. Full response:', responseText);
|
||
throw new Error('Invalid JSON response from server. Check console for details.');
|
||
}
|
||
}
|
||
|
||
console.log('=== SAVED PAGE IMPORT RESPONSE ===');
|
||
console.log('File:', file.name);
|
||
console.log('Full response:', result);
|
||
console.log('==================================');
|
||
|
||
if (!result.OK) {
|
||
throw new Error(result.MESSAGE || 'Failed to analyze saved page');
|
||
}
|
||
|
||
// Store extracted data
|
||
config.extractedData = result.DATA;
|
||
|
||
// Store image mappings for matching uploaded images to items
|
||
config.imageMappings = result.DATA.imageMappings || [];
|
||
console.log('Image mappings from saved page:', config.imageMappings.length);
|
||
|
||
// Remove loading message and start conversation flow
|
||
document.getElementById('conversation').innerHTML = '';
|
||
|
||
// Check if items have imageUrl - skip upload step if they're remote URLs (will be downloaded by saveWizard)
|
||
const itemsWithImages = (config.extractedData.items || []).filter(item => item.imageUrl).length;
|
||
const itemsWithRemoteImages = (config.extractedData.items || []).filter(item => {
|
||
if (!item.imageUrl) return false;
|
||
const url = item.imageUrl;
|
||
// Remote: http://, https://, or protocol-relative //
|
||
return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//');
|
||
}).length;
|
||
|
||
// Debug: log sample imageUrls
|
||
const sampleItems = (config.extractedData.items || []).filter(item => item.imageUrl).slice(0, 3);
|
||
console.log('Items with images:', itemsWithImages, 'Remote:', itemsWithRemoteImages);
|
||
console.log('Sample imageUrls:', sampleItems.map(i => i.imageUrl));
|
||
|
||
if (itemsWithRemoteImages > 0) {
|
||
// Items have remote image URLs - saveWizard will download them, skip upload step
|
||
console.log(`${itemsWithRemoteImages} items have remote image URLs - skipping upload step`);
|
||
if (config.businessId && config.menuId) {
|
||
showCategoriesStep();
|
||
} else {
|
||
showBusinessInfoStep();
|
||
}
|
||
} else if (itemsWithImages > 0) {
|
||
// Items have local image refs (Menu_files/) - show upload step
|
||
showImageMatchingStep();
|
||
} else if (config.businessId && config.menuId) {
|
||
// In add-menu mode, skip business info and header
|
||
showCategoriesStep();
|
||
} else {
|
||
showBusinessInfoStep();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Saved page analysis error:', error);
|
||
console.error('Full result object:', result);
|
||
document.getElementById('conversation').innerHTML = '';
|
||
let debugInfo = '';
|
||
if (result && result.debug) {
|
||
debugInfo = `<p style="font-size:12px;color:var(--gray-500);margin-top:8px;">Debug: hasHtml=${result.debug.hasHtmlKey}, htmlLen=${result.debug.htmlLength}, url="${result.debug.urlValue}"</p>`;
|
||
}
|
||
addMessage('ai', `
|
||
<p>Sorry, I encountered an error analyzing that file:</p>
|
||
<p style="color: var(--danger);">${error.message}</p>
|
||
${debugInfo}
|
||
<div class="action-buttons">
|
||
<button class="btn btn-primary" onclick="retryUrlAnalysis()">Try Again</button>
|
||
<button class="btn btn-secondary" onclick="switchToFileUpload()">Upload Images Instead</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
// Reset the input so the same file can be selected again
|
||
event.target.value = '';
|
||
}
|
||
|
||
// Legacy alias for backwards compatibility
|
||
function handleHtmlFileUpload(event) {
|
||
handleSavedPageUpload(event);
|
||
}
|
||
|
||
// Show step to upload images from saved webpage subfolder and match to items
|
||
function showImageMatchingStep() {
|
||
const itemCount = config.extractedData.items ? config.extractedData.items.length : 0;
|
||
const itemsWithImages = (config.extractedData.items || []).filter(item => item.imageUrl).length;
|
||
|
||
addMessage('ai', `
|
||
<p>I found <strong>${itemCount} menu items</strong> with <strong>${itemsWithImages} image references</strong>.</p>
|
||
<p>Upload the images from your saved webpage folder to add them to menu items.</p>
|
||
<p style="font-size:13px;color:var(--gray-500);">I'll match by Toast item ID in the filename.</p>
|
||
<div style="margin-top:16px;">
|
||
<input type="file" id="matchImagesInput" accept="image/*" multiple style="display:none;" onchange="handleImageMatching(event)">
|
||
<button class="btn btn-primary" onclick="document.getElementById('matchImagesInput').click()">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px;">
|
||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
|
||
</svg>
|
||
Upload Images from Saved Folder
|
||
</button>
|
||
<button class="btn btn-secondary" onclick="skipImageMatching()" style="margin-left:8px;">
|
||
Skip - No Images
|
||
</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
// Handle image matching from uploaded files
|
||
function handleImageMatching(event) {
|
||
const files = event.target.files;
|
||
if (!files.length) return;
|
||
|
||
let matchedCount = 0;
|
||
const matchResults = [];
|
||
const items = config.extractedData.items || [];
|
||
|
||
// Extract Toast-style item ID from filename (e.g., "item-600000000167924464_1636677556.jpg")
|
||
function extractToastId(str) {
|
||
// Match the long numeric ID pattern from Toast filenames
|
||
const match = str.match(/item-(\d{15,})/i);
|
||
return match ? match[1] : null;
|
||
}
|
||
|
||
// Check if this is a base (largest) image vs smaller variant
|
||
function isBaseSizeImage(filename) {
|
||
return !/_00[234]\.[^.]+$/i.test(filename);
|
||
}
|
||
|
||
// Group files by Toast ID, then pick best one per ID
|
||
const filesByToastId = {};
|
||
const unmatchedFiles = [];
|
||
|
||
Array.from(files).forEach(file => {
|
||
const toastId = extractToastId(file.name);
|
||
if (toastId) {
|
||
if (!filesByToastId[toastId]) {
|
||
filesByToastId[toastId] = [];
|
||
}
|
||
filesByToastId[toastId].push(file);
|
||
} else {
|
||
unmatchedFiles.push(file);
|
||
}
|
||
});
|
||
|
||
// For each Toast ID, pick the base image (no suffix) or largest file
|
||
const bestFiles = {};
|
||
for (const [toastId, fileGroup] of Object.entries(filesByToastId)) {
|
||
// Prefer base image (no _002/_003/_004 suffix)
|
||
const baseFile = fileGroup.find(f => isBaseSizeImage(f.name));
|
||
if (baseFile) {
|
||
bestFiles[toastId] = baseFile;
|
||
} else {
|
||
// Fallback: pick largest file
|
||
bestFiles[toastId] = fileGroup.reduce((a, b) => a.size > b.size ? a : b);
|
||
}
|
||
}
|
||
|
||
// Now match best files to items
|
||
for (const [toastId, file] of Object.entries(bestFiles)) {
|
||
const matchedItem = items.find(item => {
|
||
if (!item.imageUrl) return false;
|
||
const itemToastId = extractToastId(item.imageUrl);
|
||
return itemToastId === toastId;
|
||
});
|
||
|
||
if (matchedItem && !config.itemImages[matchedItem.id]) {
|
||
config.itemImages[matchedItem.id] = file;
|
||
matchedCount++;
|
||
matchResults.push({ item: matchedItem.name, file: file.name });
|
||
}
|
||
}
|
||
|
||
// Try to match unmatched files by filename in imageUrl
|
||
unmatchedFiles.forEach(file => {
|
||
const filenameBase = file.name.replace(/\.[^.]+$/, '').toLowerCase();
|
||
const matchedItem = items.find(item => {
|
||
if (!item.imageUrl) return false;
|
||
return item.imageUrl.toLowerCase().includes(filenameBase);
|
||
});
|
||
|
||
if (matchedItem && !config.itemImages[matchedItem.id]) {
|
||
config.itemImages[matchedItem.id] = file;
|
||
matchedCount++;
|
||
matchResults.push({ item: matchedItem.name, file: file.name });
|
||
}
|
||
});
|
||
|
||
// Show results
|
||
const totalFiles = files.length;
|
||
const resultHtml = matchResults.length > 0
|
||
? `<ul style="font-size:13px;margin:8px 0;max-height:200px;overflow-y:auto;">${matchResults.map(r => `<li>${r.item} ← ${r.file}</li>`).join('')}</ul>`
|
||
: '';
|
||
|
||
addMessage('ai', `
|
||
<p>Matched <strong>${matchedCount}</strong> of ${totalFiles} uploaded images to menu items.</p>
|
||
${resultHtml}
|
||
<div class="action-buttons">
|
||
<button class="btn btn-primary" onclick="proceedAfterImageMatching()">Continue</button>
|
||
<button class="btn btn-secondary" onclick="document.getElementById('matchImagesInput').click()">Upload More Images</button>
|
||
</div>
|
||
`);
|
||
|
||
console.log('Image matching results:', matchResults);
|
||
console.log('Item images map:', config.itemImages);
|
||
|
||
// Reset input
|
||
event.target.value = '';
|
||
}
|
||
|
||
function skipImageMatching() {
|
||
proceedAfterImageMatching();
|
||
}
|
||
|
||
function proceedAfterImageMatching() {
|
||
// In add-menu mode, skip business info and header
|
||
if (config.businessId && config.menuId) {
|
||
showCategoriesStep();
|
||
} else {
|
||
showBusinessInfoStep();
|
||
}
|
||
}
|
||
|
||
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.php`, {
|
||
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('========================');
|
||
|
||
// Auto-select header image from candidate indices if available
|
||
const headerCandidates = result.DATA.headerCandidateIndices || [];
|
||
if (headerCandidates.length > 0 && config.uploadedFiles.length > 0) {
|
||
const candidateIndex = headerCandidates[0];
|
||
if (candidateIndex >= 0 && candidateIndex < config.uploadedFiles.length) {
|
||
const candidateFile = config.uploadedFiles[candidateIndex];
|
||
// Only use image files as headers (not PDFs)
|
||
if (candidateFile.type.match('image.*')) {
|
||
config.headerImageFile = candidateFile;
|
||
console.log('Auto-selected header image:', candidateFile.name, 'from index', candidateIndex);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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.php`, {
|
||
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
|
||
}
|
||
}
|
||
|
||
// Helper to sync brand color inputs
|
||
function syncBrandColor(hexInput) {
|
||
let hex = hexInput.value.replace(/[^0-9A-Fa-f]/g, '').toUpperCase();
|
||
if (hex.length === 6) {
|
||
document.getElementById('bizBrandColor').value = '#' + hex;
|
||
}
|
||
}
|
||
|
||
// Sync color picker to hex input
|
||
function onBrandColorPick() {
|
||
const colorVal = document.getElementById('bizBrandColor').value;
|
||
document.getElementById('bizBrandColorHex').value = colorVal.replace('#', '').toUpperCase();
|
||
}
|
||
|
||
function syncBrandColorLight(hexInput) {
|
||
let hex = hexInput.value.replace(/[^0-9A-Fa-f]/g, '').toUpperCase();
|
||
if (hex.length === 6) {
|
||
document.getElementById('bizBrandColorLight').value = '#' + hex;
|
||
}
|
||
}
|
||
|
||
function onBrandColorLightPick() {
|
||
const colorVal = document.getElementById('bizBrandColorLight').value;
|
||
document.getElementById('bizBrandColorLightHex').value = colorVal.replace('#', '').toUpperCase();
|
||
}
|
||
|
||
// 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);
|
||
|
||
// Clean business name: strip ordering-site suffixes and embedded address
|
||
let bizDisplayName = (biz.name || '').trim();
|
||
bizDisplayName = bizDisplayName.replace(/\s*[-–—|]\s*(Order\s+(pickup|online|delivery|food)|Online\s+Order|Delivery\s*[&and]+\s*Takeout|Takeout\s*[&and]+\s*Delivery|Menu\s*[&and]+\s*Order).*$/i, '');
|
||
if (biz.addressLine1 && bizDisplayName.toLowerCase().includes(biz.addressLine1.toLowerCase())) {
|
||
bizDisplayName = bizDisplayName.replace(new RegExp(biz.addressLine1.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'), '').trim();
|
||
}
|
||
bizDisplayName = bizDisplayName.replace(/[-–—|]+$/, '').replace(/^[-–—|]+/, '').trim();
|
||
biz.name = bizDisplayName || biz.name;
|
||
|
||
// 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 || '';
|
||
|
||
// Clean city: if it contains commas (e.g. "Santa Monica, CA 90405, USA"), take just the city part
|
||
if (city.includes(',')) {
|
||
city = city.split(',')[0].trim();
|
||
}
|
||
|
||
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()
|
||
.replace(/,?\s*(United States|USA|US|U\.S\.A?\.)?\s*$/i, '').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
|
||
// Claude may return hours as an object or string — normalize to string
|
||
let hoursRaw = biz.hours || '';
|
||
if (typeof hoursRaw === 'object') {
|
||
// Try to flatten object to readable string (e.g. { monday: "9am-5pm", ... })
|
||
try { hoursRaw = Object.entries(hoursRaw).map(([k, v]) => `${k} ${v}`).join(', '); } catch (e) { hoursRaw = ''; }
|
||
}
|
||
const hoursSchedule = parseHoursString(String(hoursRaw));
|
||
|
||
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;">Brand Color</label>
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<input type="color" id="bizBrandColor" value="#${biz.brandColor || 'E74C3C'}" style="width:50px;height:36px;padding:2px;border:1px solid var(--gray-300);border-radius:6px;cursor:pointer;" onchange="onBrandColorPick()">
|
||
<input type="text" id="bizBrandColorHex" value="${biz.brandColor || 'E74C3C'}" placeholder="E74C3C" maxlength="6" style="width:80px;font-family:monospace;" oninput="syncBrandColor(this)">
|
||
<span style="font-size:11px;color:var(--gray-400);">Used for menu accents</span>
|
||
</div>
|
||
</div>
|
||
<div class="extracted-value editable">
|
||
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Brand Color Light</label>
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<input type="color" id="bizBrandColorLight" value="#${biz.brandColorLight || 'F5F5F5'}" style="width:50px;height:36px;padding:2px;border:1px solid var(--gray-300);border-radius:6px;cursor:pointer;" onchange="onBrandColorLightPick()">
|
||
<input type="text" id="bizBrandColorLightHex" value="${biz.brandColorLight || ''}" placeholder="F5F5F5" maxlength="6" style="width:80px;font-family:monospace;" oninput="syncBrandColorLight(this)">
|
||
<span style="font-size:11px;color:var(--gray-400);">Light tint for card backgrounds (optional)</span>
|
||
</div>
|
||
</div>
|
||
<div class="extracted-value editable">
|
||
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Business Hours</label>
|
||
<div style="font-size:11px;color:var(--gray-400);margin-bottom:4px;">12:00 PM = Noon • 12:00 AM = Midnight</div>
|
||
<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>
|
||
`);
|
||
|
||
// Auto-lookup tax rate based on ZIP code
|
||
if (zip && zip.length >= 5) {
|
||
lookupTaxRate(zip);
|
||
}
|
||
}
|
||
|
||
// Lookup tax rate from ZIP code and populate the field
|
||
async function lookupTaxRate(zipCode) {
|
||
const taxInput = document.getElementById('bizTaxRate');
|
||
if (!taxInput) return;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/setup/lookupTaxRate.php?zip=${encodeURIComponent(zipCode)}`);
|
||
const data = await resp.json();
|
||
|
||
if (data.OK && data.taxRate > 0) {
|
||
taxInput.value = data.taxRate;
|
||
taxInput.style.backgroundColor = '#f0fff4'; // Light green to indicate auto-filled
|
||
console.log('Tax rate auto-populated:', data.taxRate + '%');
|
||
}
|
||
} catch (err) {
|
||
console.log('Tax rate lookup failed (user can enter manually):', err);
|
||
}
|
||
}
|
||
|
||
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
|
||
// Get brand colors from hex inputs (without #)
|
||
let brandColor = document.getElementById('bizBrandColorHex').value.replace(/^#/, '').toUpperCase();
|
||
if (!/^[0-9A-F]{6}$/.test(brandColor)) brandColor = 'E74C3C'; // Default if invalid
|
||
let brandColorLight = (document.getElementById('bizBrandColorLightHex').value || '').replace(/^#/, '').toUpperCase();
|
||
if (brandColorLight && !/^[0-9A-F]{6}$/.test(brandColorLight)) brandColorLight = '';
|
||
|
||
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,
|
||
brandColor: brandColor,
|
||
brandColorLight: brandColorLight,
|
||
hoursSchedule: hoursSchedule // Send the structured schedule instead of the raw hours string
|
||
};
|
||
|
||
// Always show header image step so user can upload one
|
||
showHeaderImageStep();
|
||
}
|
||
|
||
// Header Image step - between business info and categories
|
||
function showHeaderImageStep() {
|
||
const hasAutoHeader = config.headerImageFile != null;
|
||
const headerText = hasAutoHeader
|
||
? `<p>I found an image that would work great as your menu header!</p>`
|
||
: `<p>This image appears at the top of your menu in the Payfrit app. It's the first thing customers see!</p>`;
|
||
|
||
addMessage('ai', `
|
||
<p><strong>Header Image</strong></p>
|
||
${headerText}
|
||
<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%;aspect-ratio:3/1;background:#333;border-radius:8px;margin:12px 0;background-size:cover;background-position:center;display:${hasAutoHeader ? 'block' : 'none'};overflow:hidden;"></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>
|
||
${hasAutoHeader ? 'Choose Different Image' : '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>
|
||
${hasAutoHeader ? 'Use This Image' : 'Continue'}
|
||
</button>
|
||
<button class="btn btn-secondary" onclick="skipHeaderImage()">Skip for Now</button>
|
||
</div>
|
||
`);
|
||
|
||
// If we have an auto-detected header, show its preview
|
||
if (hasAutoHeader) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
const preview = document.getElementById('headerUploadPreview');
|
||
if (preview) {
|
||
preview.style.backgroundImage = `url(${e.target.result})`;
|
||
preview.style.display = 'block';
|
||
}
|
||
};
|
||
reader.readAsDataURL(config.headerImageFile);
|
||
}
|
||
}
|
||
|
||
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() {
|
||
if (config.scratchMode) {
|
||
saveScratchBusiness();
|
||
} else {
|
||
showCategoriesStep();
|
||
}
|
||
}
|
||
|
||
function skipHeaderImage() {
|
||
config.headerImageFile = null;
|
||
if (config.scratchMode) {
|
||
saveScratchBusiness();
|
||
} else {
|
||
showCategoriesStep();
|
||
}
|
||
}
|
||
|
||
// Step 2: Categories
|
||
function renderCategoryListHtml(categories) {
|
||
return categories.map((cat, i) => {
|
||
const isSubcat = cat.parentCategoryName ? true : false;
|
||
return `
|
||
<div class="extracted-list-item" style="${isSubcat ? 'padding-left: 28px;' : ''}" data-parent="${cat.parentCategoryName || ''}">
|
||
<input type="checkbox" checked data-index="${i}">
|
||
${isSubcat ? '<span style="color: var(--gray-400); margin-right: 4px; font-size: 12px;">└</span>' : ''}
|
||
<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 showCategoriesStep() {
|
||
updateProgress(3);
|
||
const categories = config.extractedData.categories || [];
|
||
const topLevel = categories.filter(c => !c.parentCategoryName);
|
||
const subcats = categories.filter(c => c.parentCategoryName);
|
||
|
||
let label = `${topLevel.length} menu categories`;
|
||
if (subcats.length > 0) label += ` (${subcats.length} subcategories)`;
|
||
|
||
addMessage('ai', `
|
||
<p>I found <strong>${label}</strong>:</p>
|
||
<div class="extracted-list" id="categoriesList">
|
||
${renderCategoryListHtml(categories)}
|
||
</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) {
|
||
const removed = config.extractedData.categories[index];
|
||
config.extractedData.categories.splice(index, 1);
|
||
// If removing a parent, also remove its subcategories
|
||
if (removed && !removed.parentCategoryName) {
|
||
config.extractedData.categories = config.extractedData.categories.filter(
|
||
c => c.parentCategoryName !== removed.name
|
||
);
|
||
}
|
||
document.getElementById('categoriesList').innerHTML =
|
||
renderCategoryListHtml(config.extractedData.categories);
|
||
}
|
||
|
||
function addCategory() {
|
||
const input = document.getElementById('newCategoryName');
|
||
const name = input.value.trim();
|
||
if (!name) return;
|
||
|
||
config.extractedData.categories.push({ name, itemCount: 0 });
|
||
input.value = '';
|
||
|
||
document.getElementById('categoriesList').innerHTML =
|
||
renderCategoryListHtml(config.extractedData.categories);
|
||
}
|
||
|
||
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) {
|
||
const origCat = config.extractedData.categories[checkbox.dataset.index];
|
||
const catObj = {
|
||
name: nameInput.value,
|
||
itemCount: origCat?.itemCount || 0
|
||
};
|
||
// Preserve parentCategoryName for subcategories
|
||
if (origCat?.parentCategoryName) {
|
||
catObj.parentCategoryName = origCat.parentCategoryName;
|
||
}
|
||
// Preserve menuName for multi-menu mapping
|
||
if (origCat?.menuName) {
|
||
catObj.menuName = origCat.menuName;
|
||
}
|
||
updatedCategories.push(catObj);
|
||
}
|
||
});
|
||
|
||
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 showAddModifierForm() {
|
||
// Initialize modifiers array if needed
|
||
if (!config.extractedData.modifiers) {
|
||
config.extractedData.modifiers = [];
|
||
}
|
||
|
||
addMessage('ai', `
|
||
<div class="add-modifier-form">
|
||
<h4>Add Modifier Template</h4>
|
||
<div class="form-group">
|
||
<label>Modifier Name</label>
|
||
<input type="text" id="newModName" class="form-control" placeholder="e.g., Size, Add-ons, Spice Level">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" id="newModRequired"> Required (customer must choose)
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Options</label>
|
||
<div id="newModOptions">
|
||
<div class="modifier-option-row">
|
||
<input type="text" class="form-control option-name" placeholder="Option name">
|
||
<input type="number" class="form-control option-price" placeholder="Price" step="0.01" min="0">
|
||
<button class="btn btn-sm btn-outline" onclick="removeModifierOption(this)">×</button>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-sm btn-outline" onclick="addModifierOption()">+ Add Option</button>
|
||
</div>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-outline" onclick="showItemsStep()">Cancel</button>
|
||
<button class="btn btn-outline" onclick="saveModifierAndAddAnother()">Save & Add Another</button>
|
||
<button class="btn btn-primary" onclick="saveModifierAndContinue()">Save & Continue</button>
|
||
</div>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
function addModifierOption() {
|
||
const container = document.getElementById('newModOptions');
|
||
const row = document.createElement('div');
|
||
row.className = 'modifier-option-row';
|
||
row.innerHTML = `
|
||
<input type="text" class="form-control option-name" placeholder="Option name">
|
||
<input type="number" class="form-control option-price" placeholder="Price" step="0.01" min="0">
|
||
<button class="btn btn-sm btn-outline" onclick="removeModifierOption(this)">×</button>
|
||
`;
|
||
container.appendChild(row);
|
||
}
|
||
|
||
function removeModifierOption(btn) {
|
||
const row = btn.closest('.modifier-option-row');
|
||
const container = document.getElementById('newModOptions');
|
||
if (container.children.length > 1) {
|
||
row.remove();
|
||
}
|
||
}
|
||
|
||
function saveCurrentModifier() {
|
||
const name = document.getElementById('newModName').value.trim();
|
||
if (!name) {
|
||
alert('Please enter a modifier name');
|
||
return false;
|
||
}
|
||
|
||
const required = document.getElementById('newModRequired').checked;
|
||
const optionRows = document.querySelectorAll('#newModOptions .modifier-option-row');
|
||
const options = [];
|
||
|
||
optionRows.forEach(row => {
|
||
const optName = row.querySelector('.option-name').value.trim();
|
||
const optPrice = parseFloat(row.querySelector('.option-price').value) || 0;
|
||
if (optName) {
|
||
options.push({ name: optName, price: optPrice });
|
||
}
|
||
});
|
||
|
||
if (options.length === 0) {
|
||
alert('Please add at least one option');
|
||
return false;
|
||
}
|
||
|
||
config.extractedData.modifiers.push({
|
||
name: name,
|
||
required: required,
|
||
options: options,
|
||
appliesTo: 'uncertain'
|
||
});
|
||
|
||
return true;
|
||
}
|
||
|
||
function saveModifierAndAddAnother() {
|
||
if (saveCurrentModifier()) {
|
||
showAddModifierForm();
|
||
}
|
||
}
|
||
|
||
function saveModifierAndContinue() {
|
||
if (saveCurrentModifier()) {
|
||
showItemsStep();
|
||
}
|
||
}
|
||
|
||
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 || [];
|
||
|
||
// Assign unique IDs to all items (fixes per-category index mismatch in confirmItems)
|
||
items.forEach((item, idx) => {
|
||
item.id = 'item_' + idx;
|
||
});
|
||
|
||
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 (subcategories grouped under parents)
|
||
let itemsByCategory = {};
|
||
let assignedItemIds = new Set();
|
||
|
||
// Build parent-to-subcategory map
|
||
const subcatMap = {};
|
||
categories.forEach(cat => {
|
||
if (cat.parentCategoryName) {
|
||
if (!subcatMap[cat.parentCategoryName]) subcatMap[cat.parentCategoryName] = [];
|
||
subcatMap[cat.parentCategoryName].push(cat.name);
|
||
}
|
||
});
|
||
|
||
categories.forEach(cat => {
|
||
const catItems = items.filter(item => item.category === cat.name);
|
||
if (catItems.length > 0) {
|
||
itemsByCategory[cat.name] = catItems;
|
||
catItems.forEach(item => assignedItemIds.add(item.id));
|
||
}
|
||
});
|
||
|
||
// Collect any items not assigned to a known category
|
||
const unassignedItems = items.filter(item => !assignedItemIds.has(item.id));
|
||
if (unassignedItems.length > 0) {
|
||
// Group by their category name, or "Menu" if none
|
||
unassignedItems.forEach(item => {
|
||
const catName = item.category || 'Menu';
|
||
if (!itemsByCategory[catName]) {
|
||
itemsByCategory[catName] = [];
|
||
}
|
||
itemsByCategory[catName].push(item);
|
||
});
|
||
}
|
||
|
||
let itemsHtml = '';
|
||
// Render top-level categories first, then their subcategories
|
||
const topLevelCats = categories.filter(c => !c.parentCategoryName);
|
||
const renderedCats = new Set();
|
||
|
||
function renderCategoryItems(catName, isSubcat) {
|
||
const catItems = itemsByCategory[catName];
|
||
if (!catItems || catItems.length === 0) return '';
|
||
renderedCats.add(catName);
|
||
const indent = isSubcat ? 'margin-left: 20px;' : '';
|
||
const prefix = isSubcat ? '<span style="color: var(--gray-400); margin-right: 4px;">└</span>' : '';
|
||
return `
|
||
<div style="margin-bottom: 16px; ${indent}">
|
||
<h4 style="margin-bottom: 8px; color: var(--gray-700);">${prefix}${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) => {
|
||
const itemMods = item.modifiers || [];
|
||
const allMods = config.extractedData.modifiers || [];
|
||
const modGroupsHtml = itemMods.map(mName => {
|
||
const mg = allMods.find(m => m.name === mName);
|
||
const opts = mg && mg.options ? mg.options.filter(o => o && o.name) : [];
|
||
const optsHtml = opts.map(o =>
|
||
`<div class="item-mod-option">${o.name}${o.price ? `<span class="opt-price">+$${o.price.toFixed(2)}</span>` : ''}</div>`
|
||
).join('');
|
||
return `<div class="item-mod-group">
|
||
<span class="item-mod-group-header" onclick="this.querySelector('.mod-expand-btn').classList.toggle('open');this.parentElement.querySelector('.item-mod-options').classList.toggle('visible')">
|
||
<span class="mod-expand-btn">+</span> ${mName}${opts.length ? ` (${opts.length})` : ''}
|
||
</span>
|
||
<div class="item-mod-options">${optsHtml}</div>
|
||
</div>`;
|
||
}).join('');
|
||
return `
|
||
<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>` : ''}
|
||
${modGroupsHtml ? `<div class="item-mod-groups">${modGroupsHtml}</div>` : ''}
|
||
</td>
|
||
<td>$${parseFloat(item.price || 0).toFixed(2)}</td>
|
||
<td>
|
||
<div class="item-modifiers">
|
||
${itemMods.map(m => `<span class="item-modifier-tag">${m}</span>`).join('')}
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>`;
|
||
}
|
||
|
||
// Render each top-level category and its subcategories
|
||
topLevelCats.forEach(cat => {
|
||
itemsHtml += renderCategoryItems(cat.name, false);
|
||
const subs = subcatMap[cat.name] || [];
|
||
subs.forEach(subName => {
|
||
itemsHtml += renderCategoryItems(subName, true);
|
||
});
|
||
});
|
||
|
||
// Render any remaining categories not yet rendered
|
||
for (const [catName, catItems] of Object.entries(itemsByCategory)) {
|
||
if (renderedCats.has(catName) || catItems.length === 0) continue;
|
||
itemsHtml += renderCategoryItems(catName, false);
|
||
}
|
||
|
||
const hasAnyMods = items.some(it => (it.modifiers || []).length > 0);
|
||
const modsBtn = hasAnyMods ? '<button class="btn btn-outline" id="toggleModsBtn" onclick="toggleItemModifiers()">Show Modifiers</button>' : '';
|
||
|
||
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">
|
||
${modsBtn}
|
||
<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 toggleItemModifiers() {
|
||
const groups = document.querySelectorAll('#itemsList .item-mod-groups');
|
||
const btn = document.getElementById('toggleModsBtn');
|
||
const showing = btn.textContent === 'Hide Modifiers';
|
||
groups.forEach(g => g.classList.toggle('visible', !showing));
|
||
btn.textContent = showing ? 'Show Modifiers' : 'Hide Modifiers';
|
||
}
|
||
|
||
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);
|
||
}
|
||
});
|
||
|
||
// DEBUG
|
||
console.log('=== confirmItems DEBUG ===');
|
||
console.log('checkboxes found:', checkboxes.length);
|
||
console.log('checkedIds:', [...checkedIds].slice(0, 10));
|
||
console.log('items before filter:', config.extractedData.items.length);
|
||
|
||
config.extractedData.items = config.extractedData.items.filter((item, i) =>
|
||
checkedIds.has(item.id || i.toString())
|
||
);
|
||
|
||
console.log('items after filter:', config.extractedData.items.length);
|
||
|
||
// Now that user has seen items, ask about uncertain modifiers
|
||
showUncertainModifiersStep();
|
||
}
|
||
|
||
// Step 5: Final Review
|
||
function showFinalStep() {
|
||
updateProgress(6);
|
||
|
||
console.log('=== showFinalStep DEBUG ===');
|
||
console.log('config.extractedData.menus:', config.extractedData.menus);
|
||
console.log('config.extractedData keys:', Object.keys(config.extractedData));
|
||
console.log('menus length:', (config.extractedData.menus || []).length);
|
||
|
||
const { business, categories, modifiers, items } = config.extractedData;
|
||
|
||
const topCats = categories.filter(c => !c.parentCategoryName);
|
||
const subCats = categories.filter(c => c.parentCategoryName);
|
||
document.getElementById('summaryCategories').textContent = topCats.length > 0 ? topCats.length : categories.length;
|
||
const subRow = document.getElementById('summarySubcategoriesRow');
|
||
if (subCats.length > 0) {
|
||
document.getElementById('summarySubcategories').textContent = subCats.length;
|
||
subRow.style.display = '';
|
||
} else {
|
||
subRow.style.display = 'none';
|
||
}
|
||
document.getElementById('summaryModifiers').textContent = modifiers.length;
|
||
document.getElementById('summaryItems').textContent = items.length;
|
||
|
||
// Count images by type
|
||
const itemsWithImageUrl = items.filter(item => item.imageUrl).length;
|
||
const manuallyMatchedImages = Object.keys(config.itemImages || {}).length;
|
||
const hasHeaderImage = config.headerImageFile != null;
|
||
|
||
const imageParts = [];
|
||
if (itemsWithImageUrl > 0) imageParts.push(`${itemsWithImageUrl} item`);
|
||
if (manuallyMatchedImages > 0) imageParts.push(`${manuallyMatchedImages} matched`);
|
||
if (hasHeaderImage) imageParts.push('1 header');
|
||
|
||
const imagesRow = document.getElementById('summaryImagesRow');
|
||
if (imageParts.length > 0) {
|
||
document.getElementById('summaryImages').textContent = imageParts.join(', ');
|
||
imagesRow.style.display = '';
|
||
} else {
|
||
imagesRow.style.display = 'none';
|
||
}
|
||
|
||
// In add-menu mode, hide menu name/hours/community meal (already set during menu creation)
|
||
const detectedMenus = config.extractedData.menus || [];
|
||
if (config.menuId) {
|
||
document.getElementById('singleMenuSection').style.display = 'none';
|
||
document.getElementById('multiMenuSection').style.display = 'none';
|
||
document.getElementById('communityMealCard').style.display = 'none';
|
||
} else if (detectedMenus.length > 1) {
|
||
// Multiple menus detected — show multi-menu UI
|
||
document.getElementById('singleMenuSection').style.display = 'none';
|
||
document.getElementById('multiMenuSection').style.display = '';
|
||
const menuListHtml = detectedMenus.map((m, i) => {
|
||
const menuItems = (config.extractedData.items || []).filter(item => item.menu === m.name);
|
||
return `<div style="display:flex;align-items:center;gap:12px;padding:8px 12px;background:var(--gray-50);border:1px solid var(--gray-200);border-radius:6px;margin-bottom:6px;">
|
||
<input type="checkbox" id="menu_${i}" checked style="width:18px;height:18px;">
|
||
<label for="menu_${i}" style="flex:1;font-weight:500;">${m.name}</label>
|
||
<span style="color:var(--gray-500);font-size:13px;">${menuItems.length} items</span>
|
||
</div>`;
|
||
}).join('');
|
||
document.getElementById('multiMenuList').innerHTML = menuListHtml;
|
||
} else {
|
||
// Single menu — show normal UI
|
||
document.getElementById('singleMenuSection').style.display = '';
|
||
document.getElementById('multiMenuSection').style.display = 'none';
|
||
// 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;
|
||
}
|
||
}
|
||
|
||
const imagesSummary = imageParts.length > 0 ? `<li>${imageParts.join(', ')} images</li>` : '';
|
||
|
||
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>${topCats.length} categories${subCats.length > 0 ? ` (${subCats.length} subcategories)` : ''}</li>
|
||
<li>${modifiers.length} modifier templates</li>
|
||
<li>${items.length} menu items</li>
|
||
${imagesSummary}
|
||
</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 detectedMenus = config.extractedData.menus || [];
|
||
if (detectedMenus.length > 1) {
|
||
// Multi-menu mode — collect selected menus
|
||
const selectedMenus = [];
|
||
detectedMenus.forEach((m, i) => {
|
||
const cb = document.getElementById(`menu_${i}`);
|
||
if (cb && cb.checked) selectedMenus.push(m.name);
|
||
});
|
||
if (selectedMenus.length === 0) {
|
||
showToast('Please select at least one menu', 'error');
|
||
return;
|
||
}
|
||
config.extractedData.selectedMenus = selectedMenus;
|
||
config.extractedData.menuName = selectedMenus[0]; // fallback
|
||
} else {
|
||
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.php`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
businessId: config.businessId || 0,
|
||
menuId: config.menuId || 0,
|
||
userId: config.userId,
|
||
data: config.extractedData,
|
||
tempFolder: config.tempFolder
|
||
})
|
||
});
|
||
|
||
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.php`, {
|
||
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');
|
||
}
|
||
}
|
||
|
||
// Upload item images if any were matched
|
||
const itemIdMap = summary.itemIdMap || summary.ITEMIDMAP || {};
|
||
const itemImageEntries = Object.entries(config.itemImages || {});
|
||
if (itemImageEntries.length > 0) {
|
||
console.log('Uploading', itemImageEntries.length, 'item images...');
|
||
saveBtn.innerHTML = '<div class="loading-spinner" style="width:16px;height:16px;border-width:2px;"></div> Uploading images...';
|
||
|
||
let uploadedCount = 0;
|
||
let failedCount = 0;
|
||
|
||
for (const [frontendId, file] of itemImageEntries) {
|
||
// Look up database ID from the map (try frontend ID, then item name)
|
||
let dbItemId = itemIdMap[frontendId];
|
||
if (!dbItemId) {
|
||
// Try to find by item name
|
||
const item = config.extractedData.items.find(i => i.id === frontendId);
|
||
if (item && item.name) {
|
||
dbItemId = itemIdMap[item.name];
|
||
}
|
||
}
|
||
|
||
if (!dbItemId) {
|
||
console.warn('No database ID found for frontend ID:', frontendId);
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('ItemID', dbItemId);
|
||
formData.append('photo', file);
|
||
|
||
const imgResp = await fetch(`${config.apiBaseUrl}/menu/uploadItemPhoto.php`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const imgResult = await imgResp.json();
|
||
if (imgResult.OK) {
|
||
uploadedCount++;
|
||
} else {
|
||
console.error('Item image upload failed:', imgResult.MESSAGE);
|
||
failedCount++;
|
||
}
|
||
} catch (imgErr) {
|
||
console.error('Item image upload error:', imgErr);
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
console.log(`Item images: ${uploadedCount} uploaded, ${failedCount} failed`);
|
||
if (failedCount > 0) {
|
||
showToast(`${uploadedCount} images uploaded, ${failedCount} failed. You can add images later in Menu Builder.`, 'warning');
|
||
} else if (uploadedCount > 0) {
|
||
showToast(`${uploadedCount} item images uploaded!`, 'success');
|
||
}
|
||
}
|
||
|
||
// 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>
|
||
`;
|
||
|
||
// Build subcategory lookup for preview
|
||
const previewSubcatMap = {};
|
||
categories.forEach(cat => {
|
||
if (cat.parentCategoryName) {
|
||
if (!previewSubcatMap[cat.parentCategoryName]) previewSubcatMap[cat.parentCategoryName] = [];
|
||
previewSubcatMap[cat.parentCategoryName].push(cat);
|
||
}
|
||
});
|
||
const previewRendered = new Set();
|
||
|
||
function renderPreviewCategory(cat, isSubcat) {
|
||
previewRendered.add(cat.name);
|
||
const catItems = items.filter(item => item.category === cat.name);
|
||
const tag = isSubcat ? 'h3' : 'h2';
|
||
const indent = isSubcat ? 'margin-left: 20px;' : '';
|
||
let html = `
|
||
<div class="category" style="${indent}">
|
||
<${tag} onclick="this.parentElement.classList.toggle('collapsed')" ${isSubcat ? 'style="font-size: 16px;"' : ''}>
|
||
${cat.name} <span class="toggle-icon">[+/-]</span>
|
||
</${tag}>
|
||
<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 && !(previewSubcatMap[cat.name]?.length) ? '<div class="item" style="color:#999;">No items in this category</div>' : ''}
|
||
</div>
|
||
</div>`;
|
||
// Render subcategories inline
|
||
if (previewSubcatMap[cat.name]) {
|
||
previewSubcatMap[cat.name].forEach(sub => {
|
||
html += renderPreviewCategory(sub, true);
|
||
});
|
||
}
|
||
return html;
|
||
}
|
||
|
||
categories.filter(c => !c.parentCategoryName).forEach(cat => {
|
||
previewHtml += renderPreviewCategory(cat, false);
|
||
});
|
||
// Render any unrendered categories
|
||
categories.forEach(cat => {
|
||
if (!previewRendered.has(cat.name)) {
|
||
previewHtml += renderPreviewCategory(cat, false);
|
||
}
|
||
});
|
||
|
||
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.php`, {
|
||
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 = 'signup.html';
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|