Files
nabeh/backend/public/index.html
2026-05-22 02:09:48 +03:00

2103 lines
107 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nabeh Hub — WhatsApp Gateway & CRM</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta name="description" content="Connect, manage, and scale your WhatsApp communication.">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- QR Code Generator Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- AlpineJS CDN -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
:root {
--bg-color: #0b0d19;
--card-bg: rgba(20, 24, 46, 0.65);
--card-border: rgba(255, 255, 255, 0.07);
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--primary-accent: #06b6d4; /* Cyan */
--primary-accent-glow: rgba(6, 182, 212, 0.4);
--success-accent: #10b981; /* Emerald */
--success-glow: rgba(16, 185, 129, 0.3);
--warning-accent: #f59e0b; /* Amber */
--warning-glow: rgba(245, 158, 11, 0.3);
--danger-accent: #ef4444; /* Rose */
--danger-glow: rgba(239, 68, 68, 0.3);
--sidebar-width: 260px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
display: flex;
flex-direction: column;
position: relative;
}
/* Abstract glowing background patterns */
body::before {
content: '';
position: absolute;
width: 350px;
height: 350px;
top: 15%;
left: 10%;
background: radial-gradient(circle, rgba(99, 102, 241, 0.15) 0%, transparent 70%);
z-index: -1;
pointer-events: none;
}
body::after {
content: '';
position: absolute;
width: 450px;
height: 450px;
bottom: 10%;
right: 5%;
background: radial-gradient(circle, var(--primary-accent-glow) 0%, transparent 70%);
z-index: -1;
pointer-events: none;
}
h1, h2, h3, h4 {
font-family: 'Outfit', sans-serif;
font-weight: 700;
}
a, button, input {
font-family: inherit;
}
/* Container & Layout */
.app-container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
flex: 1;
display: flex;
flex-direction: column;
}
/* Glassmorphism Auth Card */
.auth-wrapper {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-height: 80vh;
}
.auth-card {
background: var(--card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 2.5rem;
width: 100%;
max-width: 460px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
animation: fadeIn 0.5s ease-out;
}
.brand-header {
text-align: center;
margin-bottom: 2rem;
}
.brand-logo {
font-size: 2.2rem;
font-weight: 800;
background: linear-gradient(135deg, #fff 30%, var(--primary-accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.05em;
margin-bottom: 0.5rem;
}
.brand-subtitle {
font-size: 0.9rem;
color: var(--text-secondary);
}
/* Tabs styling */
.tabs-header {
display: flex;
border-bottom: 1px solid var(--card-border);
margin-bottom: 1.5rem;
}
.tab-btn {
flex: 1;
padding: 0.8rem;
text-align: center;
background: none;
border: none;
color: var(--text-secondary);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab-btn.active {
color: var(--text-primary);
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 2px;
background: var(--primary-accent);
box-shadow: 0 0 10px var(--primary-accent-glow);
}
/* Inputs & Form control */
.form-group {
margin-bottom: 1.25rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 500;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
background: rgba(10, 11, 20, 0.5);
border: 1px solid var(--card-border);
border-radius: 8px;
color: var(--text-primary);
font-size: 0.95rem;
transition: all 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: var(--primary-accent);
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.15);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 0.8rem 1.5rem;
font-weight: 600;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.95rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-accent) 0%, #0891b2 100%);
color: #fff;
box-shadow: 0 4px 14px var(--primary-accent-glow);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px var(--primary-accent-glow);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary);
border: 1px solid var(--card-border);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.12);
}
.btn-danger {
background: linear-gradient(135deg, var(--danger-accent) 0%, #dc2626 100%);
color: #fff;
box-shadow: 0 4px 14px var(--danger-glow);
}
.btn-danger:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px var(--danger-glow);
}
/* Message banners */
.banner {
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
font-size: 0.85rem;
display: flex;
align-items: center;
animation: slideDown 0.3s ease-out;
}
.banner-danger {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
.banner-success {
background: rgba(16, 185, 129, 0.15);
border: 1px solid rgba(16, 185, 129, 0.3);
color: #d1fae5;
}
/* Dashboard layout */
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--card-border);
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
.user-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-accent) 0%, #4f46e5 100%);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-family: 'Outfit', sans-serif;
color: #fff;
}
.dashboard-layout {
display: grid;
grid-template-columns: 240px 1fr;
gap: 2rem;
flex: 1;
}
@media (max-width: 900px) {
.dashboard-layout {
grid-template-columns: 1fr;
}
}
/* Navigation menu */
.nav-menu {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
@media (max-width: 900px) {
.nav-menu {
flex-direction: row;
overflow-x: auto;
padding-bottom: 0.5rem;
}
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.8rem 1rem;
border-radius: 10px;
background: none;
border: none;
color: var(--text-secondary);
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
}
.nav-item:hover, .nav-item.active {
background: rgba(255, 255, 255, 0.05);
color: var(--text-primary);
}
.nav-item.active {
box-shadow: inset 3px 0 0 var(--primary-accent);
background: rgba(6, 182, 212, 0.08);
}
@media (max-width: 900px) {
.nav-item.active {
box-shadow: inset 0 -3px 0 var(--primary-accent);
}
}
/* Dashboard content panels */
.panel {
background: var(--card-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: fadeIn 0.4s ease-out;
}
.grid-two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
@media (max-width: 768px) {
.grid-two {
grid-template-columns: 1fr;
}
}
/* Status indicators */
.status-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
border-radius: 12px;
background: rgba(10, 11, 20, 0.4);
border: 1px solid var(--card-border);
text-align: center;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1.5rem;
}
.badge-disconnected {
background: rgba(239, 68, 68, 0.12);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.25);
}
.badge-connecting {
background: rgba(6, 182, 212, 0.12);
color: #22d3ee;
border: 1px solid rgba(6, 182, 212, 0.25);
animation: pulse 1.5s infinite alternate;
}
.badge-waiting_qr {
background: rgba(245, 158, 11, 0.12);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.25);
animation: pulse-border 1.5s infinite;
}
.badge-connected {
background: rgba(16, 185, 129, 0.12);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.25);
box-shadow: 0 0 15px rgba(16, 185, 129, 0.2);
}
/* QR Code wrapper */
.qr-wrapper {
background: #fff;
padding: 1.25rem;
border-radius: 12px;
display: inline-flex;
justify-content: center;
align-items: center;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
margin: 1.5rem 0;
position: relative;
}
/* Lists and Tables */
.data-table-container {
width: 100%;
overflow-x: auto;
margin-top: 1rem;
}
.data-table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.data-table th {
padding: 1rem;
border-bottom: 1px solid var(--card-border);
color: var(--text-secondary);
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
}
.data-table td {
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
font-size: 0.9rem;
}
.data-table tr:hover td {
background: rgba(255, 255, 255, 0.02);
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-secondary);
}
/* Spinner / Loading styles */
.spinner {
border: 3px solid rgba(255, 255, 255, 0.1);
width: 24px;
height: 24px;
border-radius: 50%;
border-left-color: var(--primary-accent);
animation: spin 0.8s linear infinite;
display: inline-block;
}
.spinner-large {
width: 48px;
height: 48px;
border-width: 4px;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes pulse {
0% { opacity: 0.6; }
100% { opacity: 1; }
}
@keyframes pulse-border {
0% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); }
100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); }
}
/* Utility classes */
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.gap-1 { gap: 0.25rem; }
.text-center { text-align: center; }
.font-semibold { font-weight: 600; }
.text-muted { color: var(--text-secondary); }
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(10, 11, 20, 0.85);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
animation: fadeIn 0.3s ease-out;
}
.modal-card {
background: var(--card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--card-border);
border-radius: 16px;
width: 100%;
max-width: 500px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
animation: slideDown 0.3s ease-out;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
font-size: 1.2rem;
font-weight: 700;
}
.modal-close {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.5rem;
cursor: pointer;
transition: color 0.2s ease;
}
.modal-close:hover {
color: var(--text-primary);
}
.modal-body {
padding: 1.5rem;
overflow-y: auto;
max-height: 70vh;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--card-border);
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.font-semibold { font-weight: 600; }
.text-muted { color: var(--text-secondary); }
@keyframes pulse-red {
0% { transform: scale(0.85); opacity: 0.5; }
50% { transform: scale(1.15); opacity: 1; }
100% { transform: scale(0.85); opacity: 0.5; }
}
.recording-pulse {
animation: pulse-red 1.2s infinite;
}
</style>
</head>
<body x-data="app()" x-init="checkAuth()">
<div class="app-container">
<!-- Auth View -->
<template x-if="!isLoggedIn">
<div class="auth-wrapper">
<div class="auth-card">
<div class="brand-header">
<div class="brand-logo" id="main-brand-logo" style="display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 0.75rem;">
<img src="/logo.svg" alt="Nabeh Logo" style="width: 48px; height: 48px; border-radius: 12px; box-shadow: 0 0 15px rgba(6, 182, 212, 0.4);">
<span style="font-family: 'Outfit', sans-serif; font-size: 2.2rem; font-weight: 800; background: linear-gradient(135deg, #fff 30%, var(--primary-accent) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.05em;">Nabeh</span>
</div>
<div class="brand-subtitle">WhatsApp Hub & Marketing Automation</div>
</div>
<!-- Alert Banners -->
<template x-if="authError">
<div class="banner banner-danger" id="auth-error-banner">
<span x-text="authError"></span>
</div>
</template>
<template x-if="authSuccess">
<div class="banner banner-success" id="auth-success-banner">
<span x-text="authSuccess"></span>
</div>
</template>
<!-- Tabs Header -->
<div class="tabs-header">
<button class="tab-btn" :class="{ 'active': authTab === 'login' }" @click="authTab = 'login'; authError = ''; authSuccess = ''" id="tab-login-btn">Login</button>
<button class="tab-btn" :class="{ 'active': authTab === 'register' }" @click="authTab = 'register'; authError = ''; authSuccess = ''" id="tab-register-btn">Register</button>
</div>
<!-- Login Form -->
<form x-show="authTab === 'login'" @submit.prevent="login()" id="login-form">
<div class="form-group">
<label class="form-label">Email Address</label>
<input type="email" class="form-input" x-model="loginEmail" required placeholder="admin@company.com" id="login-email-input">
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" class="form-input" x-model="loginPassword" required placeholder="••••••••" id="login-password-input">
</div>
<button type="submit" class="btn btn-primary" :disabled="actionLoading" id="login-submit-btn">
<span x-show="!actionLoading">Sign In</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</form>
<!-- Register Form -->
<form x-show="authTab === 'register'" @submit.prevent="register()" id="register-form">
<div class="form-group">
<label class="form-label">Company Name</label>
<input type="text" class="form-input" x-model="regCompanyName" required placeholder="Acme Corporation" id="register-company-input">
</div>
<div class="form-group">
<label class="form-label">Administrator Name</label>
<input type="text" class="form-input" x-model="regUserName" required placeholder="John Doe" id="register-name-input">
</div>
<div class="form-group">
<label class="form-label">Email Address</label>
<input type="email" class="form-input" x-model="regEmail" required placeholder="admin@company.com" id="register-email-input">
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" class="form-input" x-model="regPassword" required placeholder="••••••••" id="register-password-input">
</div>
<button type="submit" class="btn btn-primary" :disabled="actionLoading" id="register-submit-btn">
<span x-show="!actionLoading">Create Account</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</form>
</div>
</div>
</template>
<!-- Main Dashboard View -->
<template x-if="isLoggedIn">
<div>
<div class="dashboard-header" style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 16px;">
<img src="/logo.svg" alt="Nabeh Logo" style="width: 52px; height: 52px; border-radius: 12px; box-shadow: 0 0 15px rgba(6, 182, 212, 0.35); border: 1px solid rgba(255, 255, 255, 0.08);">
<div>
<h1 style="font-size: 1.8rem; margin: 0 0 0.15rem 0;">Nabeh Dashboard</h1>
<p class="text-muted" style="font-size: 0.9rem; margin: 0;">Connected to system: <span class="font-semibold" x-text="user.name"></span></p>
</div>
</div>
<div class="user-info">
<div class="avatar" x-text="user.name.charAt(0).toUpperCase()"></div>
<button @click="logout()" class="btn btn-secondary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" id="logout-btn">Log Out</button>
</div>
</div>
<div class="dashboard-layout">
<!-- Left Sidebar Nav -->
<div class="nav-menu">
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'whatsapp' }" @click="activeDashboardTab = 'whatsapp'" id="nav-whatsapp-btn">
<span>📱</span> WhatsApp Connection
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'contacts' }" @click="activeDashboardTab = 'contacts'; fetchContacts(); fetchGroups()" id="nav-contacts-btn">
<span>👥</span> Contacts Directory
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'templates' }" @click="activeDashboardTab = 'templates'; fetchTemplates()" id="nav-templates-btn">
<span>📝</span> Message Templates
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'campaigns' }" @click="activeDashboardTab = 'campaigns'; fetchCampaigns()" id="nav-campaigns-btn">
<span>📣</span> Marketing Campaigns
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'chatbot' }" @click="activeDashboardTab = 'chatbot'; fetchChatbotSettings()" id="nav-chatbot-btn">
<span>🤖</span> AI Chatbot Settings
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'integrations' }" @click="activeDashboardTab = 'integrations'; fetchEndpoints()" id="nav-integrations-btn">
<span>🔌</span> API Integrations
</button>
</div>
<!-- Right Dashboard Panels -->
<div style="flex: 1;">
<!-- Panel: WhatsApp Connection -->
<div class="panel" x-show="activeDashboardTab === 'whatsapp'" id="panel-whatsapp">
<h2 style="font-size: 1.4rem; margin-bottom: 1.5rem;">WhatsApp Integration</h2>
<div class="grid-two">
<!-- Connection Control Card -->
<div class="status-box">
<div class="status-badge" :class="{
'badge-disconnected': !whatsappSession || whatsappSession.status === 'disconnected',
'badge-connecting': whatsappSession && whatsappSession.status === 'connecting',
'badge-waiting_qr': whatsappSession && whatsappSession.status === 'waiting_qr',
'badge-connected': whatsappSession && whatsappSession.status === 'connected'
}">
<span x-text="whatsappSession ? whatsappSession.status : 'disconnected'"></span>
</div>
<h3 style="font-size: 1.25rem; margin-bottom: 0.5rem;">
<template x-if="whatsappSession && whatsappSession.status === 'connected'">
<span>WhatsApp Connected</span>
</template>
<template x-if="!whatsappSession || whatsappSession.status !== 'connected'">
<span>Session Inactive</span>
</template>
</h3>
<p class="text-muted" style="font-size: 0.9rem; margin-bottom: 1.5rem; max-width: 250px;">
<template x-if="whatsappSession && whatsappSession.phone">
<span>Active number: <strong x-text="whatsappSession.phone" style="color: var(--text-primary);"></strong></span>
</template>
<template x-if="!whatsappSession || !whatsappSession.phone">
<span>Start a connection session to link your phone.</span>
</template>
</p>
<!-- Actions -->
<template x-if="!whatsappSession || whatsappSession.status === 'disconnected'">
<button @click="connectWhatsapp()" class="btn btn-primary" :disabled="actionLoading" id="btn-request-qr">
<span x-show="!actionLoading">Generate QR Code</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</template>
<template x-if="whatsappSession && whatsappSession.status !== 'disconnected'">
<button @click="disconnectWhatsapp()" class="btn btn-danger" :disabled="actionLoading" id="btn-disconnect-session">
<span x-show="!actionLoading">Disconnect Session</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</template>
</div>
<!-- QR Display Card -->
<div class="flex-center flex-column panel" style="background: rgba(10, 11, 20, 0.2); border-color: rgba(255, 255, 255, 0.03);" id="qr-display-container">
<template x-if="whatsappSession && whatsappSession.status === 'connecting'">
<div class="text-center">
<div class="spinner spinner-large" style="margin-bottom: 1rem;"></div>
<p class="font-semibold">Establishing Connection...</p>
<p class="text-muted" style="font-size: 0.85rem; margin-top: 0.25rem;">Checking gateway processes and requesting channel</p>
</div>
</template>
<template x-if="whatsappSession && whatsappSession.status === 'waiting_qr'">
<div class="text-center">
<p class="font-semibold">Scan QR Code</p>
<p class="text-muted" style="font-size: 0.85rem; margin-top: 0.25rem;">Scan using Link Devices inside WhatsApp</p>
<div class="qr-wrapper">
<div id="qrcode-canvas" x-init="$nextTick(() => renderQr())"></div>
</div>
<!-- Diagnostics -->
<div style="font-size: 0.75rem; margin: 0.5rem 0; display: flex; flex-direction: column; gap: 0.25rem;">
<template x-if="!whatsappSession.qr_code">
<span style="color: var(--danger-accent);">⚠️ Decryption issue: QR code string is empty.</span>
</template>
<template x-if="whatsappSession.qr_code">
<span style="color: var(--success-accent);">✓ Encrypted QR data retrieved successfully.</span>
</template>
<template x-if="typeof window.QRCode === 'undefined'">
<span style="color: var(--danger-accent);">⚠️ QRCode library failed to load (Integrity/CSP issue).</span>
</template>
</div>
<div style="font-size: 0.8rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;" class="text-muted">
<div class="spinner" style="width: 14px; height: 14px; border-width: 2px;"></div>
<span>Waiting for connection handshake...</span>
</div>
</div>
</template>
<template x-if="whatsappSession && whatsappSession.status === 'connected'">
<div class="text-center" style="padding: 2rem 0;">
<div style="font-size: 3rem; color: var(--success-accent); margin-bottom: 0.5rem; text-shadow: 0 0 20px var(--success-glow);"></div>
<p class="font-semibold" style="font-size: 1.15rem; color: var(--success-accent);">Gateway fully connected</p>
<p class="text-muted" style="font-size: 0.85rem; margin-top: 0.25rem; max-width: 250px; margin-left: auto; margin-right: auto;">
You can now create templates and launch marketing broadcast campaigns.
</p>
</div>
</template>
<template x-if="!whatsappSession || whatsappSession.status === 'disconnected'">
<div class="text-center text-muted" style="padding: 3rem 0;">
<span style="font-size: 2.5rem; display: block; margin-bottom: 0.5rem;">🔌</span>
<p>No active WhatsApp link</p>
</div>
</template>
</div>
</div>
</div>
<!-- Panel: Contacts Directory -->
<div class="panel" x-show="activeDashboardTab === 'contacts'" id="panel-contacts">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
<h2 style="font-size: 1.4rem;">Contacts Directory</h2>
<button class="btn btn-primary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" @click="openAddContactModal()" id="btn-add-contact">+ Add Contact</button>
</div>
<!-- Bulk Action Bar -->
<div x-show="selectedContactIds.length > 0" class="bulk-action-bar" style="background: rgba(6, 182, 212, 0.1); border: 1px solid var(--primary-accent); padding: 1rem; border-radius: 10px; margin-bottom: 1.5rem; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem; animation: fadeIn 0.3s ease-out;">
<span style="font-size: 0.9rem; color: var(--text-primary); font-weight: 600;">
Selected: <span x-text="selectedContactIds.length" style="color: var(--primary-accent);"></span> contact(s)
</span>
<div style="display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;">
<select class="form-input" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" x-model="bulkGroupId" @focus="fetchGroups()">
<option value="">-- Add to Existing Group --</option>
<template x-for="grp in groups" :key="grp.id">
<option :value="grp.id" x-text="grp.name"></option>
</template>
</select>
<span style="color: var(--text-secondary); font-size: 0.85rem;">or</span>
<input type="text" class="form-input" style="width: 180px; padding: 0.5rem 1rem; font-size: 0.85rem;" placeholder="New Group Name" x-model="bulkNewGroupName">
<button class="btn btn-primary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" @click="applyBulkGroup()">Apply Grouping</button>
</div>
</div>
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px; text-align: center;">
<input type="checkbox" @change="selectedContactIds = $event.target.checked ? contacts.map(c => c.id) : []" :checked="contacts.length > 0 && selectedContactIds.length === contacts.length" style="cursor: pointer; width: 16px; height: 16px;">
</th>
<th>Name</th>
<th>Phone</th>
<th>Email</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<template x-for="contact in contacts" :key="contact.id">
<tr>
<td style="text-align: center;">
<input type="checkbox" :value="contact.id" x-model="selectedContactIds" style="cursor: pointer; width: 16px; height: 16px;">
</td>
<td class="font-semibold" x-text="contact.name"></td>
<td x-text="contact.phone || 'N/A'"></td>
<td x-text="contact.email || 'N/A'"></td>
<td>
<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background-color: var(--success-accent); margin-right: 0.25rem;"></span>
<span>Active</span>
</td>
</tr>
</template>
</tbody>
</table>
<template x-if="contacts.length === 0">
<div class="empty-state">No contacts added yet.</div>
</template>
</div>
</div>
<!-- Panel: Message Templates -->
<div class="panel" x-show="activeDashboardTab === 'templates'" id="panel-templates">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
<h2 style="font-size: 1.4rem;">Templates</h2>
<button class="btn btn-primary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" @click="openNewTemplateModal()" id="btn-add-template">+ New Template</button>
</div>
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th>Template Name</th>
<th>Category</th>
<th>Language</th>
<th>Variables</th>
</tr>
</thead>
<tbody>
<template x-for="tpl in templates" :key="tpl.id">
<tr>
<td class="font-semibold" x-text="tpl.name"></td>
<td x-text="tpl.category || 'Utility'"></td>
<td x-text="tpl.language || 'en'"></td>
<td x-text="tpl.variables || 'None'"></td>
</tr>
</template>
</tbody>
</table>
<template x-if="templates.length === 0">
<div class="empty-state">No templates created yet.</div>
</template>
</div>
</div>
<!-- Panel: Campaigns -->
<div class="panel" x-show="activeDashboardTab === 'campaigns'" id="panel-campaigns">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
<h2 style="font-size: 1.4rem;">Campaigns</h2>
<button class="btn btn-primary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" @click="openLaunchCampaignModal()" id="btn-add-campaign">+ Launch Campaign</button>
</div>
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th>Campaign Name</th>
<th>Template</th>
<th>Status</th>
<th>Sent Count</th>
</tr>
</thead>
<tbody>
<template x-for="cmp in campaigns" :key="cmp.id">
<tr>
<td class="font-semibold" x-text="cmp.name"></td>
<td x-text="cmp.template_name || 'N/A'"></td>
<td>
<span class="status-badge" :class="cmp.status === 'completed' ? 'badge-connected' : 'badge-connecting'" style="margin: 0; padding: 0.2rem 0.5rem; font-size: 0.75rem;" x-text="cmp.status"></span>
</td>
<td x-text="cmp.sent_count || 0"></td>
</tr>
</template>
</tbody>
</table>
<template x-if="campaigns.length === 0">
<div class="empty-state">No campaigns created yet.</div>
</template>
</div>
</div>
<!-- Panel: AI Chatbot Settings -->
<div class="panel" x-show="activeDashboardTab === 'chatbot'" id="panel-chatbot">
<h2 style="font-size: 1.4rem; margin-bottom: 1.5rem;">AI Chatbot & Auto-Reply Settings</h2>
<form @submit.prevent="saveChatbotSettings()" id="chatbot-form">
<div class="form-group">
<label class="form-label">Enable Chatbot</label>
<select class="form-input" x-model="chatbotSettings.is_active" id="chatbot-active-select">
<option value="1">Active</option>
<option value="0">Disabled</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Auto-Reply Type</label>
<select class="form-input" x-model="chatbotSettings.trigger_type" id="chatbot-trigger-type-select">
<option value="keyword">Keyword-Match (Static reply)</option>
<option value="gemini_ai">Gemini AI (Dynamic conversational responder)</option>
</select>
</div>
<div class="form-group" x-show="chatbotSettings.trigger_type === 'keyword'" id="chatbot-keyword-group">
<label class="form-label">Trigger Keywords (Comma separated)</label>
<input type="text" class="form-input" x-model="chatbotSettings.keyword" placeholder="hello, price, discount, support" id="chatbot-keyword-input">
</div>
<div class="form-group" x-show="chatbotSettings.trigger_type === 'gemini_ai'" id="chatbot-api-key-group">
<label class="form-label">Google Gemini API Key</label>
<input type="password" class="form-input" x-model="chatbotSettings.gemini_api_key" placeholder="••••••••" id="chatbot-api-key-input">
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;">
Leave empty to use the system default API key configured in .env.
</span>
</div>
<div class="form-group" id="chatbot-prompt-group">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap; gap: 0.5rem;">
<label class="form-label" x-text="chatbotSettings.trigger_type === 'gemini_ai' ? 'System Instruction Prompt' : 'Predefined Auto-Reply Message'" style="margin-bottom: 0;"></label>
<div x-show="chatbotSettings.trigger_type === 'gemini_ai'" style="display: flex; gap: 0.35rem; flex-wrap: wrap; align-items: center;">
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('nabeh')">قالب تطبيق نبيه (سوري)</button>
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('store')">قالب متجر إلكتروني</button>
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('general')">قالب عام (إنجليزي)</button>
<!-- Voice Recording Button -->
<button type="button" class="btn" :class="isRecording ? 'btn-danger' : 'btn-secondary'" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto; display: flex; align-items: center; gap: 0.25rem;" @click="toggleVoiceRecording()">
<svg x-show="!isRecording" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
<span x-show="isRecording" class="recording-pulse" style="display: inline-block; width: 8px; height: 8px; background-color: white; border-radius: 50%;"></span>
<span x-text="isRecording ? 'إيقاف التسجيل (' + formatRecordTime(recordingTime) + ')' : '🎤 تسجيل الإرشادات'"></span>
</button>
</div>
</div>
<textarea class="form-input" x-model="chatbotSettings.ai_prompt" rows="7" required :placeholder="chatbotSettings.trigger_type === 'gemini_ai' ? 'أدخل تعليمات الذكاء الاصطناعي هنا...' : 'شكراً لتواصلك معنا!'" id="chatbot-prompt-input" :disabled="generatingFromVoice"></textarea>
<!-- Voice Processing Loading Status -->
<div x-show="generatingFromVoice" style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; color: var(--primary-accent);" dir="rtl">
<span class="spinner" style="border-top-color: var(--primary-accent); display: inline-block;"></span>
<span>جاري صياغة التوجيهات من صوتك باستخدام الذكاء الاصطناعي... يرجى الانتظار</span>
</div>
<!-- Hints & Guidelines for Merchant -->
<div x-show="chatbotSettings.trigger_type === 'gemini_ai'" style="margin-top: 0.75rem; padding: 0.75rem; background: rgba(59, 130, 246, 0.05); border: 1px dashed rgba(59, 130, 246, 0.3); border-radius: 8px; font-size: 0.8rem; color: var(--text-secondary);" dir="rtl">
<strong style="color: var(--text-primary); display: block; margin-bottom: 0.35rem; display: flex; align-items: center; gap: 0.35rem;">
💡 نصائح وتوجيهات لكتابة تعليمات ممتازة:
</strong>
<ul style="list-style-type: disc; margin-right: 1.25rem; padding-left: 0; line-height: 1.5; margin-bottom: 0;">
<li><strong>الهوية والاسم:</strong> حدد اسم الروبوت بوضوح (مثال: "أنا سارة من فريق تطبيق نبيه").</li>
<li><strong>اللهجة والأسلوب:</strong> اطلب من الذكاء الاصطناعي الرد بلهجة معينة (مثال: اللهجة السورية أو الفصحى المبسطة).</li>
<li><strong>البيانات الأساسية:</strong> اكتب ساعات عمل المتجر، طرق الدفع والتوصيل، وسياسة الاستبدال لكي يجيب الروبوت بدقة.</li>
<li><strong>التعليمات اللغوية:</strong> قمنا بتضمين ميزة مطابقة اللغة تلقائياً (الرد بالإنجليزية على الرسائل الإنجليزية، وبالعربية على العربية).</li>
</ul>
</div>
</div>
<button type="submit" class="btn btn-primary" :disabled="actionLoading" id="chatbot-save-btn">
<span x-show="!actionLoading">Save Settings</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</form>
</div>
<!-- Panel: API Integrations -->
<div class="panel" x-show="activeDashboardTab === 'integrations'" id="panel-integrations">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 style="font-size: 1.4rem; margin: 0;">API Endpoints Integration</h2>
<button class="btn btn-primary" style="width: auto;" @click="endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', headers: '' }; showAddEndpointModal = true" id="add-endpoint-btn">
+ Add API Integration
</button>
</div>
<p class="text-muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;">
Configure external web APIs for multi-tenant integrations. The chatbot can fetch user profiles or verify payment details dynamically.
</p>
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th>Integration Name</th>
<th>Action Type</th>
<th>Endpoint URL</th>
<th>Description</th>
<th style="width: 150px; text-align: center;">Actions</th>
</tr>
</thead>
<tbody>
<template x-for="endpoint in endpoints" :key="endpoint.id">
<tr>
<td class="font-semibold" x-text="endpoint.name"></td>
<td>
<span class="status-badge" :class="endpoint.action_type === 'verify_payment' ? 'badge-waiting_qr' : 'badge-connecting'" style="margin: 0; padding: 0.2rem 0.5rem; font-size: 0.75rem;" x-text="endpoint.action_type === 'verify_payment' ? 'Verify Payment' : 'Fetch User Info'"></span>
</td>
<td style="font-family: monospace; font-size: 0.85rem;" x-text="endpoint.endpoint_url"></td>
<td x-text="endpoint.description || 'No description provided.'"></td>
<td style="text-align: center; display: flex; gap: 0.5rem; justify-content: center; align-items: center;">
<button class="btn btn-secondary" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="editEndpoint(endpoint)" :id="'edit-endpoint-btn-' + endpoint.id">Edit</button>
<button class="btn btn-danger" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="deleteEndpoint(endpoint.id)" :id="'delete-endpoint-btn-' + endpoint.id">Delete</button>
</td>
</tr>
</template>
</tbody>
</table>
<template x-if="endpoints.length === 0">
<div class="empty-state" id="empty-endpoints-state">No API endpoints configured yet. Connect Intaleq or Salla integrations.</div>
</template>
</div>
</div>
</div>
</div>
<!-- Modal: Add Contact -->
<div class="modal-overlay" x-show="showAddContactModal" id="modal-add-contact" style="display: none;">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title">Add New Contact</h3>
<button class="modal-close" @click="showAddContactModal = false">&times;</button>
</div>
<form @submit.prevent="submitAddContact()" id="form-add-contact">
<div class="modal-body">
<div class="form-group">
<label class="form-label">Name</label>
<input type="text" class="form-input" x-model="contactName" required placeholder="John Doe" id="add-contact-name">
</div>
<div class="form-group">
<label class="form-label">Phone Number (with Country Code)</label>
<input type="text" class="form-input" x-model="contactPhone" required placeholder="966500000000" id="add-contact-phone">
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" x-model="contactEmail" placeholder="john@example.com" id="add-contact-email">
</div>
<div class="form-group">
<label class="form-label">Notes</label>
<textarea class="form-input" x-model="contactNotes" placeholder="Additional details..." id="add-contact-notes"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showAddContactModal = false">Cancel</button>
<button type="submit" class="btn btn-primary" style="width: auto;" :disabled="actionLoading">
<span x-show="!actionLoading">Create Contact</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</div>
</form>
</div>
</div>
<!-- Modal: Add/Edit Endpoint Integration -->
<div class="modal-overlay" x-show="showAddEndpointModal" id="modal-add-endpoint" style="display: none;">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title" x-text="endpointForm.id ? 'Edit API Endpoint Integration' : 'Add API Endpoint Integration'"></h3>
<button class="modal-close" @click="showAddEndpointModal = false">&times;</button>
</div>
<form @submit.prevent="submitAddEndpoint()" id="form-add-endpoint">
<div class="modal-body">
<div class="form-group">
<label class="form-label">Integration Name</label>
<input type="text" class="form-input" x-model="endpointForm.name" required placeholder="e.g. Intaleq Driver Lookup" id="add-endpoint-name">
</div>
<div class="form-group">
<label class="form-label">Endpoint URL</label>
<input type="url" class="form-input" x-model="endpointForm.endpoint_url" required placeholder="https://yourdomain.com/api/nabeh/action" id="add-endpoint-url">
</div>
<div class="form-group">
<label class="form-label">Action Type</label>
<select class="form-input" x-model="endpointForm.action_type" required id="add-endpoint-action-type">
<option value="verify_payment">Verify Payment (تحليل وتأكيد الدفع)</option>
<option value="fetch_user_info">Fetch User Info (جلب معلومات السائق أو العميل)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea class="form-input" x-model="endpointForm.description" placeholder="A brief explanation of what this endpoint does..." id="add-endpoint-description"></textarea>
</div>
<div class="form-group">
<label class="form-label">Custom HTTP Headers (JSON Object)</label>
<textarea class="form-input" style="font-family: monospace; font-size: 0.85rem; height: 120px;" x-model="endpointForm.headers" placeholder='{&#10; "Authorization": "Bearer YOUR_SECRET_TOKEN",&#10; "X-Custom-Source": "Nabeh App"&#10;}' id="add-endpoint-headers"></textarea>
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;">
Optionally define any authorization tokens or custom headers in JSON format.
</span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showAddEndpointModal = false">Cancel</button>
<button type="submit" class="btn btn-primary" style="width: auto;" :disabled="actionLoading">
<span x-show="!actionLoading" x-text="endpointForm.id ? 'Save Changes' : 'Create Integration'"></span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</div>
</form>
</div>
</div>
<!-- Modal: New Template -->
<div class="modal-overlay" x-show="showNewTemplateModal" id="modal-new-template" style="display: none;">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title">Create Message Template</h3>
<button class="modal-close" @click="showNewTemplateModal = false">&times;</button>
</div>
<form @submit.prevent="submitNewTemplate()" id="form-new-template">
<div class="modal-body">
<div class="form-group">
<label class="form-label">Template Name</label>
<input type="text" class="form-input" x-model="templateName" required placeholder="welcome_message" id="new-template-name">
</div>
<div class="form-group">
<label class="form-label">Message Body</label>
<textarea class="form-input" x-model="templateBody" rows="4" required placeholder="Hello {{name}}, welcome to Nabeh!" id="new-template-body"></textarea>
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;">
Use {{name}} to personalize with the contact's name.
</span>
</div>
<div class="form-group">
<label class="form-label">Template Type</label>
<select class="form-input" x-model="templateType" id="new-template-type">
<option value="text">Text Only</option>
<option value="image">Image Attachment</option>
<option value="video">Video Attachment</option>
<option value="document">Document Attachment</option>
</select>
</div>
<div class="form-group" x-show="templateType !== 'text'" id="new-template-media-group">
<label class="form-label">Media URL</label>
<input type="url" class="form-input" x-model="templateMediaUrl" placeholder="https://example.com/image.jpg" id="new-template-media-url">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showNewTemplateModal = false">Cancel</button>
<button type="submit" class="btn btn-primary" style="width: auto;" :disabled="actionLoading">
<span x-show="!actionLoading">Create Template</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</div>
</form>
</div>
</div>
<!-- Modal: Launch Campaign -->
<div class="modal-overlay" x-show="showLaunchCampaignModal" id="modal-launch-campaign" style="display: none;">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title">Launch Marketing Campaign</h3>
<button class="modal-close" @click="showLaunchCampaignModal = false">&times;</button>
</div>
<form @submit.prevent="submitLaunchCampaign()" id="form-launch-campaign">
<div class="modal-body">
<div class="form-group">
<label class="form-label">Campaign Name</label>
<input type="text" class="form-input" x-model="campaignName" required placeholder="Ramadan Campaign 2026" id="launch-campaign-name">
</div>
<div class="form-group">
<label class="form-label">Target Group</label>
<select class="form-input" x-model="campaignGroupId" required id="launch-campaign-group">
<option value="">-- Select Contact Group --</option>
<template x-for="grp in groups" :key="grp.id">
<option :value="grp.id" x-text="grp.name"></option>
</template>
</select>
</div>
<div class="form-group">
<label class="form-label">WhatsApp Session Sender</label>
<select class="form-input" x-model="campaignSessionId" required id="launch-campaign-session">
<option value="">-- Select Sender Number --</option>
<template x-if="whatsappSession">
<option :value="whatsappSession.id" x-text="whatsappSession.name + ' (' + (whatsappSession.phone || 'Connected') + ')'"></option>
</template>
</select>
</div>
<div class="form-group">
<label class="form-label">Message Template</label>
<select class="form-input" x-model="campaignTemplateId" required id="launch-campaign-template">
<option value="">-- Select Template --</option>
<template x-for="tpl in templates" :key="tpl.id">
<option :value="tpl.id" x-text="tpl.name"></option>
</template>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showLaunchCampaignModal = false">Cancel</button>
<button type="submit" class="btn btn-primary" style="width: auto;" :disabled="actionLoading">
<span x-show="!actionLoading">Launch Broadcast</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</div>
</form>
</div>
</div>
</div>
</template>
</div>
<script>
function app() {
return {
// Auth States
isLoggedIn: false,
authTab: 'login',
loginEmail: '',
loginPassword: '',
regCompanyName: '',
regUserName: '',
regEmail: '',
regPassword: '',
authError: '',
authSuccess: '',
token: null,
user: null,
// Dashboard States
activeDashboardTab: 'whatsapp',
whatsappSession: null,
contacts: [],
selectedContactIds: [],
bulkGroupId: '',
bulkNewGroupName: '',
templates: [],
campaigns: [],
pollingIntervalId: null,
actionLoading: false,
// Modals
showAddContactModal: false,
showNewTemplateModal: false,
showLaunchCampaignModal: false,
showAddEndpointModal: false,
// Endpoint Form
endpointForm: {
id: null,
name: '',
endpoint_url: '',
action_type: 'verify_payment',
description: '',
headers: ''
},
endpoints: [],
// Forms
contactName: '',
contactPhone: '',
contactEmail: '',
contactNotes: '',
templateName: '',
templateBody: '',
templateType: 'text',
templateMediaUrl: '',
campaignName: '',
campaignGroupId: '',
campaignSessionId: '',
campaignTemplateId: '',
groups: [],
// Voice Recording States
isRecording: false,
recordingTime: 0,
recordingIntervalId: null,
mediaRecorder: null,
audioChunks: [],
generatingFromVoice: false,
chatbotSettings: {
is_active: '0',
trigger_type: 'keyword',
keyword: '',
ai_prompt: '',
gemini_api_key: ''
},
// Methods
checkAuth() {
this.token = localStorage.getItem('nabeh_token');
const storedUser = localStorage.getItem('nabeh_user');
if (this.token && storedUser) {
this.user = JSON.parse(storedUser);
this.isLoggedIn = true;
this.initializeDashboard();
}
},
async login() {
this.actionLoading = true;
this.authError = '';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: this.loginEmail, password: this.loginPassword })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('nabeh_token', data.token);
localStorage.setItem('nabeh_user', JSON.stringify(data.user));
this.token = data.token;
this.user = data.user;
this.isLoggedIn = true;
this.initializeDashboard();
} else {
this.authError = data.error || 'Authentication failed. Please verify credentials.';
}
} catch (err) {
this.authError = 'Connection failed. Please check server availability.';
} finally {
this.actionLoading = false;
}
},
async register() {
this.actionLoading = true;
this.authError = '';
this.authSuccess = '';
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
company_name: this.regCompanyName,
user_name: this.regUserName,
email: this.regEmail,
password: this.regPassword
})
});
const data = await response.json();
if (response.ok) {
this.authSuccess = 'Registration successful! Please log in.';
this.authTab = 'login';
this.loginEmail = this.regEmail;
// Clear form values
this.regCompanyName = '';
this.regUserName = '';
this.regEmail = '';
this.regPassword = '';
} else {
if (data.errors) {
// Extract first validation error
const firstKey = Object.keys(data.errors)[0];
this.authError = data.errors[firstKey][0];
} else {
this.authError = data.error || 'Registration failed.';
}
}
} catch (err) {
this.authError = 'Connection failed. Please try again.';
} finally {
this.actionLoading = false;
}
},
logout() {
this.stopPolling();
localStorage.removeItem('nabeh_token');
localStorage.removeItem('nabeh_user');
this.token = null;
this.user = null;
this.isLoggedIn = false;
this.whatsappSession = null;
this.contacts = [];
this.templates = [];
this.campaigns = [];
this.endpoints = [];
},
initializeDashboard() {
this.fetchWhatsappStatus();
// Set up persistent background status check
this.startPolling();
},
async fetchWhatsappStatus() {
if (!this.token) return;
try {
const response = await fetch('/api/whatsapp/status', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok && data.status === 'success') {
this.whatsappSession = data.data;
if (this.whatsappSession && this.whatsappSession.status === 'waiting_qr') {
this.$nextTick(() => this.renderQr());
}
}
} catch (err) {
console.error('Failed to retrieve session status:', err);
}
},
async connectWhatsapp() {
this.actionLoading = true;
try {
const response = await fetch('/api/whatsapp/qr', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok && data.status === 'success') {
await this.fetchWhatsappStatus();
} else {
alert(data.message || 'Failed to initialize session');
}
} catch (err) {
alert('Error communicating with backend Gateway API.');
} finally {
this.actionLoading = false;
}
},
async disconnectWhatsapp() {
if (!confirm('Are you sure you want to disconnect your WhatsApp link?')) return;
this.actionLoading = true;
try {
const response = await fetch('/api/whatsapp/disconnect', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok && data.status === 'success') {
await this.fetchWhatsappStatus();
}
} catch (err) {
console.error(err);
} finally {
this.actionLoading = false;
}
},
renderQr() {
const canvasDiv = document.getElementById('qrcode-canvas');
console.log('renderQr() invoked. canvasDiv:', canvasDiv, 'Session data:', this.whatsappSession);
if (!canvasDiv || !this.whatsappSession || !this.whatsappSession.qr_code) {
return;
}
try {
if (typeof window.QRCode === 'undefined') {
throw new Error('QRCode class is not defined. Script resource failed to load.');
}
// Clear previous QR instance
canvasDiv.innerHTML = '';
new QRCode(canvasDiv, {
text: this.whatsappSession.qr_code,
width: 200,
height: 200,
colorDark: "#0b0d19",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
console.log('QR Code generated successfully.');
} catch (e) {
console.error('Error generating QR code:', e);
}
},
startPolling() {
this.stopPolling();
// Poll session state every 4 seconds
this.pollingIntervalId = setInterval(() => {
this.fetchWhatsappStatus();
}, 4000);
},
stopPolling() {
if (this.pollingIntervalId) {
clearInterval(this.pollingIntervalId);
this.pollingIntervalId = null;
}
},
// Custom API Integrations CRUD
async fetchEndpoints() {
try {
const response = await fetch('/api/endpoints', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok) {
this.endpoints = data.data || [];
}
} catch (err) {
console.error('Error fetching endpoints:', err);
}
},
async submitAddEndpoint() {
this.actionLoading = true;
try {
if (this.endpointForm.headers && this.endpointForm.headers.trim()) {
try {
JSON.parse(this.endpointForm.headers);
} catch (jsonErr) {
alert('Invalid JSON in Custom Headers. Please verify format.');
this.actionLoading = false;
return;
}
}
const response = await fetch('/api/endpoints', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify(this.endpointForm)
});
const data = await response.json();
if (response.ok) {
this.showAddEndpointModal = false;
this.endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', headers: '' };
await this.fetchEndpoints();
} else {
alert(data.message || 'Failed to save integration endpoint.');
}
} catch (err) {
console.error('Error saving endpoint:', err);
} finally {
this.actionLoading = false;
}
},
editEndpoint(endpoint) {
this.endpointForm = {
id: endpoint.id,
name: endpoint.name,
endpoint_url: endpoint.endpoint_url,
action_type: endpoint.action_type,
description: endpoint.description || '',
headers: endpoint.headers || ''
};
this.showAddEndpointModal = true;
},
async deleteEndpoint(id) {
if (!confirm('Are you sure you want to delete this integration endpoint?')) return;
this.actionLoading = true;
try {
const response = await fetch('/api/endpoints', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({ id: id })
});
const data = await response.json();
if (response.ok) {
await this.fetchEndpoints();
} else {
alert(data.message || 'Failed to delete endpoint.');
}
} catch (err) {
console.error('Error deleting endpoint:', err);
} finally {
this.actionLoading = false;
}
},
// CRM List Fetchers
async fetchContacts() {
this.selectedContactIds = [];
this.bulkGroupId = '';
this.bulkNewGroupName = '';
try {
const response = await fetch('/api/contacts', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok) {
this.contacts = data.contacts || data.data || [];
}
} catch (err) {
console.error('Error fetching contacts:', err);
}
},
async applyBulkGroup() {
if (this.selectedContactIds.length === 0) {
alert('Please select at least one contact first.');
return;
}
if (!this.bulkGroupId && !this.bulkNewGroupName.trim()) {
alert('Please select an existing group or type a new group name.');
return;
}
this.actionLoading = true;
try {
let groupId = this.bulkGroupId;
// If a new group name is typed, create the group first
if (this.bulkNewGroupName.trim()) {
const newGroupRes = await fetch('/api/groups', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({ name: this.bulkNewGroupName.trim() })
});
const newGroupData = await newGroupRes.json();
if (!newGroupRes.ok) {
throw new Error(newGroupData.message || newGroupData.error || 'Failed to create group');
}
groupId = newGroupData.id;
}
// Attach selected contacts in bulk
const bulkRes = await fetch('/api/groups/bulk-add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({
group_id: groupId,
contact_ids: this.selectedContactIds
})
});
const bulkData = await bulkRes.json();
if (bulkRes.ok) {
alert('Successfully added selected contacts to the group!');
this.selectedContactIds = [];
this.bulkGroupId = '';
this.bulkNewGroupName = '';
await this.fetchGroups();
} else {
alert(bulkData.error || bulkData.message || 'Failed to apply grouping.');
}
} catch (err) {
console.error('Error applying bulk grouping:', err);
alert(err.message || 'An error occurred during bulk grouping.');
} finally {
this.actionLoading = false;
}
},
async fetchTemplates() {
try {
const response = await fetch('/api/templates', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok) {
this.templates = data.templates || data.data || [];
}
} catch (err) {
console.error('Error fetching templates:', err);
}
},
async fetchCampaigns() {
try {
const response = await fetch('/api/campaigns', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok) {
this.campaigns = data.campaigns || data.data || [];
}
} catch (err) {
console.error('Error fetching campaigns:', err);
}
},
async fetchGroups() {
try {
const response = await fetch('/api/groups', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok) {
this.groups = data.groups || data.data || [];
}
} catch (err) {
console.error('Error fetching groups:', err);
}
},
async fetchChatbotSettings() {
try {
const response = await fetch('/api/chatbot/rules', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok && data.data && data.data.length > 0) {
const rule = data.data[0];
this.chatbotSettings.is_active = rule.is_active.toString();
this.chatbotSettings.trigger_type = rule.trigger_type;
this.chatbotSettings.keyword = rule.keyword || '';
this.chatbotSettings.ai_prompt = rule.ai_prompt || '';
this.chatbotSettings.gemini_api_key = rule.gemini_api_key ? '••••••••' : '';
} else {
this.chatbotSettings = {
is_active: '0',
trigger_type: 'keyword',
keyword: '',
ai_prompt: '',
gemini_api_key: ''
};
}
} catch (err) {
console.error('Error fetching chatbot settings:', err);
}
},
loadPromptTemplate(type) {
if (type === 'nabeh') {
this.chatbotSettings.ai_prompt = `أنتِ "سارة"، موظفة خدمة العملاء الافتراضية الذكية والودودة لتطبيق "نبيه" (Nabeh).
مهمتكِ هي مساعدة المستخدمين والإجابة على استفساراتهم بلطف وأدب بالمسائل التقنية والتجارية المتعلقة بالتطبيق.
اتبعي القواعد التالية بدقة عند الرد:
1. في أول رسالة تواصل مع العميل، عرّفي عن نفسكِ دائماً بالقول: "معك سارة من فريق تطبيق نبيه، كيف بقدر أساعدك اليوم؟" (باللهجة السورية الودية).
2. تحدثي دائماً باللهجة السورية اللطيفة والترحيبية والودية عند التحدث باللغة العربية.
3. إذا سأل العميل أو تحدث باللغة الإنجليزية، فتحدثي معه باللغة الإنجليزية بشكل احترافي وودي وحافظي على نفس الهوية والمساعدة.
4. كوني مختصرة ومباشرة في إجاباتكِ وتجنبي الإطالة غير الضرورية.
5. ساعدي المستخدمين في فهم ميزات تطبيق "نبيه" لإدارة وتسهيل حملات واتساب والردود الذكية.`;
} else if (type === 'store') {
this.chatbotSettings.ai_prompt = `أنت موظف خدمة عملاء ذكي ومرحب لمتجرنا الإلكتروني.
مهمتك هي مساعدة العملاء والإجابة على استفساراتهم حول المنتجات، الشحن، والطلبات بلطف وأدب.
اتبع القواعد التالية عند الرد:
1. رحب بالعميل دائماً بشكل ودّي وسريع في بداية المحادثة.
2. أجب عن الأسئلة بدقة واختصار.
3. إذا سأل العميل عن حالة طلب، اطلب منه تزويدك برقم الطلب للتحقق منه.
4. حافظ على نبرة إيجابية ومحترفة.`;
} else if (type === 'general') {
this.chatbotSettings.ai_prompt = `You are a professional and helpful customer support assistant.
Your goal is to assist users with their general inquiries, troubleshoot issues, and provide helpful guidance.
Guidelines:
1. Be polite, clear, and professional.
2. Provide concise and accurate information.
3. If you do not know the answer, politely ask the user to wait while you check with the team.`;
}
},
async saveChatbotSettings() {
this.actionLoading = true;
try {
const payload = {
is_active: this.chatbotSettings.is_active === '1',
trigger_type: this.chatbotSettings.trigger_type,
keyword: this.chatbotSettings.keyword,
ai_prompt: this.chatbotSettings.ai_prompt,
gemini_api_key: this.chatbotSettings.gemini_api_key,
session_id: this.whatsappSession ? this.whatsappSession.id : null
};
const response = await fetch('/api/chatbot/rules', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok) {
alert('Chatbot settings saved successfully.');
await this.fetchChatbotSettings();
} else {
alert(data.message || 'Failed to save chatbot settings.');
}
} catch (err) {
alert('Error communicating with backend API.');
} finally {
this.actionLoading = false;
}
},
formatRecordTime(seconds) {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${s < 10 ? '0' : ''}${s}`;
},
async toggleVoiceRecording() {
if (this.isRecording) {
await this.stopVoiceRecording();
} else {
await this.startVoiceRecording();
}
},
async startVoiceRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.audioChunks = [];
this.mediaRecorder = new MediaRecorder(stream);
this.mediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
this.audioChunks.push(event.data);
}
};
this.mediaRecorder.onstop = async () => {
const mimeType = this.mediaRecorder.mimeType || 'audio/webm';
const audioBlob = new Blob(this.audioChunks, { type: mimeType });
// Stop all audio tracks to release microphone
stream.getTracks().forEach(track => track.stop());
await this.sendAudioToBackend(audioBlob);
};
this.mediaRecorder.start();
this.isRecording = true;
this.recordingTime = 0;
this.recordingIntervalId = setInterval(() => {
this.recordingTime++;
if (this.recordingTime >= 180) { // 3 minutes max limit
this.stopVoiceRecording();
}
}, 1000);
} catch (err) {
console.error('Error starting audio recording:', err);
alert('تعذر الوصول إلى الميكروفون. يرجى التحقق من صلاحيات المتصفح.');
}
},
async stopVoiceRecording() {
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
this.mediaRecorder.stop();
}
if (this.recordingIntervalId) {
clearInterval(this.recordingIntervalId);
this.recordingIntervalId = null;
}
this.isRecording = false;
},
async sendAudioToBackend(audioBlob) {
this.generatingFromVoice = true;
try {
const formData = new FormData();
formData.append('audio', audioBlob, 'voice_instruction.webm');
const response = await fetch('/api/chatbot/generate-prompt-from-audio', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`
},
body: formData
});
const data = await response.json();
if (response.ok && data.status === 'success') {
this.chatbotSettings.ai_prompt = data.prompt;
// Automatically save the generated prompt in database
await this.saveChatbotSettings();
alert('تم توليد التوجيهات وحفظها بنجاح!');
} else {
alert(data.message || 'فشلت عملية صياغة التوجيهات من الصوت.');
}
} catch (err) {
console.error('Error sending audio to backend:', err);
alert('حدث خطأ أثناء التواصل مع السيرفر لصياغة التوجيهات.');
} finally {
this.generatingFromVoice = false;
}
},
openAddContactModal() {
this.contactName = '';
this.contactPhone = '';
this.contactEmail = '';
this.contactNotes = '';
this.showAddContactModal = true;
},
async submitAddContact() {
this.actionLoading = true;
try {
const response = await fetch('/api/contacts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: this.contactName,
phone: this.contactPhone,
email: this.contactEmail,
notes: this.contactNotes
})
});
const data = await response.json();
if (response.ok) {
this.showAddContactModal = false;
await this.fetchContacts();
} else {
alert(data.error || 'Failed to create contact.');
}
} catch (err) {
alert('Connection error.');
} finally {
this.actionLoading = false;
}
},
openNewTemplateModal() {
this.templateName = '';
this.templateBody = '';
this.templateType = 'text';
this.templateMediaUrl = '';
this.showNewTemplateModal = true;
},
async submitNewTemplate() {
this.actionLoading = true;
try {
const response = await fetch('/api/templates', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: this.templateName,
body: this.templateBody,
type: this.templateType,
media_url: this.templateMediaUrl
})
});
const data = await response.json();
if (response.ok) {
this.showNewTemplateModal = false;
await this.fetchTemplates();
} else {
alert(data.error || 'Failed to create template.');
}
} catch (err) {
alert('Connection error.');
} finally {
this.actionLoading = false;
}
},
async openLaunchCampaignModal() {
this.campaignName = '';
this.campaignGroupId = '';
this.campaignSessionId = this.whatsappSession ? this.whatsappSession.id : '';
this.campaignTemplateId = '';
await this.fetchGroups();
await this.fetchTemplates();
this.showLaunchCampaignModal = true;
},
async submitLaunchCampaign() {
this.actionLoading = true;
try {
const response = await fetch('/api/campaigns', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: this.campaignName,
group_id: this.campaignGroupId,
session_id: this.campaignSessionId,
template_id: this.campaignTemplateId
})
});
const data = await response.json();
if (response.ok) {
this.showLaunchCampaignModal = false;
await this.fetchCampaigns();
} else {
alert(data.error || 'Failed to launch campaign.');
}
} catch (err) {
alert('Connection error.');
} finally {
this.actionLoading = false;
}
}
}
}
</script>
</body>
</html>