Major Changes: 1. Fixed infinite loop in wizard flow - uncertain modifiers step now correctly advances to final review instead of looping back to items 2. Moved uncertain modifier assignment to AFTER items review (makes more sense for user to see items first) 3. Added detailed modifier visualization on uncertain modifiers step showing: - Source image indicator (which image the modifier was extracted from) - Full list of all options with prices - Required/optional status - Option count summary Technical Details: - Backend: Added sourceImageIndex tracking in analyzeMenuImages.cfm to record which image each modifier came from - Frontend: Enhanced uncertain modifiers step with inline detailed view showing complete modifier structure - Flow correction: showUncertainModifiersStep() now calls showFinalStep() instead of showItemsStep() to prevent loop - Improved error handling in API calls with detailed error messages from Claude API Flow Changes: - Old: Upload → Business → Categories → Modifiers → Uncertain Modifiers → Items → [LOOP] - New: Upload → Business → Categories → Modifiers → Items → Uncertain Modifiers → Final Review Model Configuration: - Using claude-sonnet-4-20250514 for menu image analysis - Added better error reporting to surface API issues (auth, credits, etc.) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
437 lines
12 KiB
HTML
437 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Login - Payfrit Business Portal</title>
|
|
<link rel="stylesheet" href="portal.css">
|
|
<style>
|
|
body {
|
|
background: var(--bg-primary);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.login-container {
|
|
width: 100%;
|
|
max-width: 420px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.login-card {
|
|
background: var(--bg-card);
|
|
border-radius: 16px;
|
|
padding: 40px;
|
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.login-header {
|
|
text-align: center;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.login-logo {
|
|
width: 64px;
|
|
height: 64px;
|
|
background: var(--primary);
|
|
color: #000;
|
|
border-radius: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
margin: 0 auto 16px;
|
|
}
|
|
|
|
.login-header h1 {
|
|
font-size: 24px;
|
|
margin: 0 0 8px;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.login-header p {
|
|
color: var(--text-muted);
|
|
margin: 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.login-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.form-group label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.form-group input {
|
|
padding: 14px 16px;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
color: var(--text-primary);
|
|
font-size: 15px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.form-group input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
|
|
}
|
|
|
|
.form-group input::placeholder {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.login-btn {
|
|
padding: 14px 24px;
|
|
background: var(--primary);
|
|
color: #000;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.login-btn:hover {
|
|
background: var(--primary-hover);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.login-btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.login-error {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
color: #ef4444;
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
display: none;
|
|
}
|
|
|
|
.login-error.show {
|
|
display: block;
|
|
}
|
|
|
|
.login-footer {
|
|
text-align: center;
|
|
margin-top: 24px;
|
|
padding-top: 24px;
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
.login-footer a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.login-footer a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.business-select {
|
|
padding: 14px 16px;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
color: var(--text-primary);
|
|
font-size: 15px;
|
|
width: 100%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.business-select:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.step {
|
|
display: none;
|
|
}
|
|
|
|
.step.active {
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="login-container">
|
|
<div class="login-card">
|
|
<div class="login-header">
|
|
<div class="login-logo">P</div>
|
|
<h1>Business Portal</h1>
|
|
<p>Sign in to manage your business</p>
|
|
</div>
|
|
|
|
<!-- Step 1: Login -->
|
|
<div class="step active" id="step-login">
|
|
<form class="login-form" id="loginForm">
|
|
<div class="login-error" id="loginError"></div>
|
|
|
|
<div class="form-group">
|
|
<label for="username">Email or Phone</label>
|
|
<input type="text" id="username" name="username" placeholder="Enter your email or phone" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Password</label>
|
|
<input type="password" id="password" name="password" placeholder="Enter your password" required>
|
|
</div>
|
|
|
|
<button type="submit" class="login-btn" id="loginBtn">Sign In</button>
|
|
</form>
|
|
|
|
<div class="login-footer">
|
|
<a href="/index.cfm?mode=forgot">Forgot password?</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Select Business -->
|
|
<div class="step" id="step-business">
|
|
<form class="login-form" id="businessForm">
|
|
<div class="form-group">
|
|
<label for="businessSelect">Select Business</label>
|
|
<select id="businessSelect" class="business-select" required>
|
|
<option value="">Choose a business...</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button type="submit" class="login-btn" id="continueBtn">Continue</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Detect base path for API calls (handles local dev at /biz.payfrit.com/)
|
|
const BASE_PATH = (() => {
|
|
const path = window.location.pathname;
|
|
const portalIndex = path.indexOf('/portal/');
|
|
if (portalIndex > 0) {
|
|
return path.substring(0, portalIndex);
|
|
}
|
|
return '';
|
|
})();
|
|
|
|
const PortalLogin = {
|
|
userToken: null,
|
|
userId: null,
|
|
businesses: [],
|
|
|
|
init() {
|
|
// Check if already logged in
|
|
const savedToken = localStorage.getItem('payfrit_portal_token');
|
|
const savedBusiness = localStorage.getItem('payfrit_portal_business');
|
|
|
|
if (savedToken && savedBusiness) {
|
|
// Verify token is still valid
|
|
this.verifyAndRedirect(savedToken, savedBusiness);
|
|
return;
|
|
}
|
|
|
|
// Setup form handlers
|
|
document.getElementById('loginForm').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.login();
|
|
});
|
|
|
|
document.getElementById('businessForm').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.selectBusiness();
|
|
});
|
|
},
|
|
|
|
async verifyAndRedirect(token, businessId) {
|
|
// For now, just redirect - could add token verification
|
|
window.location.href = BASE_PATH + `/portal/index.html?bid=${businessId}`;
|
|
},
|
|
|
|
async login() {
|
|
const username = document.getElementById('username').value.trim();
|
|
const password = document.getElementById('password').value;
|
|
const errorEl = document.getElementById('loginError');
|
|
const btn = document.getElementById('loginBtn');
|
|
|
|
errorEl.classList.remove('show');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Signing in...';
|
|
|
|
try {
|
|
const response = await fetch(BASE_PATH + '/api/auth/login.cfm', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
console.log("[Login] Response status:", response.status);
|
|
const text = await response.text();
|
|
console.log("[Login] Raw response:", text.substring(0, 500));
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch (parseErr) {
|
|
console.error("[Login] JSON parse error:", parseErr);
|
|
errorEl.textContent = "Server error - check browser console (F12)";
|
|
errorEl.classList.add("show");
|
|
btn.disabled = false;
|
|
btn.textContent = "Sign In";
|
|
return;
|
|
}
|
|
|
|
if (data.OK && data.Token) {
|
|
this.userToken = data.Token;
|
|
this.userId = data.UserID;
|
|
|
|
// Save token
|
|
localStorage.setItem('payfrit_portal_token', data.Token);
|
|
localStorage.setItem('payfrit_portal_userid', data.UserID);
|
|
|
|
// Load user's businesses
|
|
await this.loadBusinesses();
|
|
|
|
if (this.businesses.length === 0) {
|
|
// No businesses - go directly to wizard
|
|
this.startNewRestaurant();
|
|
} else {
|
|
// Show business selection (even if just one, so they can access wizard)
|
|
this.showStep('business');
|
|
}
|
|
} else {
|
|
errorEl.textContent = data.ERROR || data.MESSAGE || 'Invalid credentials';
|
|
errorEl.classList.add('show');
|
|
}
|
|
} catch (err) {
|
|
console.error('[Login] Error:', err);
|
|
errorEl.textContent = 'Connection error. Please try again.';
|
|
errorEl.classList.add('show');
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Sign In';
|
|
},
|
|
|
|
async loadBusinesses() {
|
|
try {
|
|
console.log('[Login] Loading businesses for UserID:', this.userId);
|
|
const bizResponse = await fetch(BASE_PATH + '/api/portal/myBusinesses.cfm', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-User-Token': this.userToken
|
|
},
|
|
body: JSON.stringify({ UserID: this.userId })
|
|
});
|
|
|
|
console.log("[Login] Businesses response status:", bizResponse.status);
|
|
const bizText = await bizResponse.text();
|
|
console.log("[Login] Businesses raw response:", bizText.substring(0, 500));
|
|
|
|
let bizData;
|
|
try {
|
|
bizData = JSON.parse(bizText);
|
|
} catch (parseErr) {
|
|
console.error("[Login] Businesses JSON parse error:", parseErr);
|
|
return;
|
|
}
|
|
|
|
if (bizData.OK && bizData.BUSINESSES) {
|
|
this.businesses = bizData.BUSINESSES;
|
|
console.log('[Login] Loaded', this.businesses.length, 'businesses');
|
|
this.populateBusinessSelect();
|
|
} else {
|
|
console.error('[Login] Businesses API error:', bizData.ERROR || bizData.MESSAGE);
|
|
}
|
|
} catch (err) {
|
|
console.error('[Login] Error loading businesses:', err);
|
|
}
|
|
},
|
|
|
|
populateBusinessSelect() {
|
|
const select = document.getElementById('businessSelect');
|
|
|
|
if (this.businesses.length === 0) {
|
|
select.innerHTML = '<option value="">No businesses yet - use wizard below</option>';
|
|
select.disabled = true;
|
|
document.getElementById('continueBtn').disabled = true;
|
|
} else {
|
|
select.innerHTML = '<option value="">Choose a business...</option>';
|
|
select.disabled = false;
|
|
document.getElementById('continueBtn').disabled = false;
|
|
|
|
this.businesses.forEach(biz => {
|
|
const option = document.createElement('option');
|
|
option.value = biz.BusinessID;
|
|
option.textContent = biz.BusinessName;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
// Add "New Business Wizard" option at the end
|
|
const wizardOption = document.createElement('option');
|
|
wizardOption.value = 'NEW_WIZARD';
|
|
wizardOption.textContent = '✨ New Business Wizard';
|
|
wizardOption.style.fontWeight = 'bold';
|
|
wizardOption.style.color = 'var(--primary)';
|
|
select.appendChild(wizardOption);
|
|
}
|
|
},
|
|
|
|
showStep(step) {
|
|
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
|
|
document.getElementById(`step-${step}`).classList.add('active');
|
|
},
|
|
|
|
selectBusiness() {
|
|
const businessId = document.getElementById('businessSelect').value;
|
|
if (businessId === 'NEW_WIZARD') {
|
|
this.startNewRestaurant();
|
|
} else if (businessId) {
|
|
this.selectBusinessById(businessId);
|
|
}
|
|
},
|
|
|
|
selectBusinessById(businessId) {
|
|
localStorage.setItem('payfrit_portal_business', businessId);
|
|
window.location.href = BASE_PATH + `/portal/index.html?bid=${businessId}`;
|
|
},
|
|
|
|
startNewRestaurant() {
|
|
// Clear any existing business selection
|
|
localStorage.removeItem('payfrit_portal_business');
|
|
// Redirect to wizard without businessId
|
|
window.location.href = BASE_PATH + `/portal/setup-wizard.html`;
|
|
},
|
|
|
|
logout() {
|
|
localStorage.removeItem('payfrit_portal_token');
|
|
localStorage.removeItem('payfrit_portal_userid');
|
|
localStorage.removeItem('payfrit_portal_business');
|
|
window.location.href = BASE_PATH + '/portal/login.html';
|
|
}
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => PortalLogin.init());
|
|
</script>
|
|
</body>
|
|
</html>
|