Files
nabeh/backend/public/index.html
2026-05-22 00:12:07 +03:00

1726 lines
81 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>
<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); }
</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">Nabeh</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">
<div>
<h1 style="font-size: 1.8rem; margin-bottom: 0.25rem;">Nabeh Dashboard</h1>
<p class="text-muted" style="font-size: 0.9rem;">Connected to system: <span class="font-semibold" x-text="user.name"></span></p>
</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>
</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">
<label class="form-label" x-text="chatbotSettings.trigger_type === 'gemini_ai' ? 'System Instruction Prompt' : 'Predefined Auto-Reply Message'"></label>
<textarea class="form-input" x-model="chatbotSettings.ai_prompt" rows="5" required :placeholder="chatbotSettings.trigger_type === 'gemini_ai' ? 'You are a helpful customer support assistant... Respond concisely and politely in Arabic.' : 'Thank you for reaching out!'" id="chatbot-prompt-input"></textarea>
</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>
</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: 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,
// Forms
contactName: '',
contactPhone: '',
contactEmail: '',
contactNotes: '',
templateName: '',
templateBody: '',
templateType: 'text',
templateMediaUrl: '',
campaignName: '',
campaignGroupId: '',
campaignSessionId: '',
campaignTemplateId: '',
groups: [],
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 = [];
},
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;
}
},
// 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);
}
},
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;
}
},
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>