1135 lines
48 KiB
HTML
1135 lines
48 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-CNgIRecGo7nOMxObXtLZHyYy34UNrJDzFGaPZ55S56sxM57SL7gOhv4/dIncsaYRiLJHdU8mHgGybkx9thStTA==" 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; }
|
|
.gap-2 { gap: 0.5rem; }
|
|
.gap-3 { gap: 0.75rem; }
|
|
.gap-4 { gap: 1rem; }
|
|
|
|
.text-center { text-align: center; }
|
|
.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()" 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>
|
|
</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>
|
|
|
|
<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>
|
|
|
|
<div class="data-table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<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 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;" 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;" 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>
|
|
|
|
</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: [],
|
|
templates: [],
|
|
campaigns: [],
|
|
pollingIntervalId: null,
|
|
actionLoading: false,
|
|
|
|
// 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');
|
|
if (!canvasDiv || !this.whatsappSession || !this.whatsappSession.qr_code) return;
|
|
|
|
// 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
|
|
});
|
|
},
|
|
|
|
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() {
|
|
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 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|