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

2284 lines
123 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;
}
/* RTL Overrides */
body[dir="rtl"] .nav-item {
text-align: right;
flex-direction: row;
}
body[dir="rtl"] .data-table {
text-align: right;
}
body[dir="rtl"] .modal-header {
flex-direction: row-reverse;
}
body[dir="rtl"] .form-group {
text-align: right;
}
body[dir="rtl"] .form-label {
text-align: right;
display: block;
}
body[dir="rtl"] .dashboard-header {
flex-direction: row-reverse;
}
body[dir="rtl"] .user-info {
flex-direction: row-reverse;
}
body[dir="rtl"] .modal-footer {
flex-direction: row-reverse;
}
</style>
</head>
<body x-data="app()" x-init="checkAuth()" :dir="lang === 'ar' ? 'rtl' : 'ltr'">
<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;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<div style="display: flex; align-items: center; gap: 16px;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<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 :style="lang === 'ar' ? 'text-align: right;' : ''">
<h1 style="font-size: 1.8rem; margin: 0 0 0.15rem 0;" x-text="lang === 'ar' ? 'لوحة تحكم نبيه' : 'Nabeh Dashboard'"></h1>
<p class="text-muted" style="font-size: 0.9rem; margin: 0;">
<span x-text="lang === 'ar' ? 'متصل بالنظام باسم:' : 'Connected to system:'"></span>
<span class="font-semibold" x-text="user.name"></span>
</p>
</div>
</div>
<div class="user-info" style="display: flex; align-items: center; gap: 12px;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<button @click="toggleLang()" class="btn btn-secondary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" id="lang-toggle-btn">
<span x-text="lang === 'ar' ? 'English 🇬🇧' : 'العربية 🇸🇦'"></span>
</button>
<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" x-text="lang === 'ar' ? 'تسجيل الخروج' : '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> <span x-text="lang === 'ar' ? 'اتصال الواتساب' : 'WhatsApp Connection'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'contacts' }" @click="activeDashboardTab = 'contacts'; fetchContacts(); fetchGroups()" id="nav-contacts-btn">
<span>👥</span> <span x-text="lang === 'ar' ? 'دليل جهات الاتصال' : 'Contacts Directory'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'templates' }" @click="activeDashboardTab = 'templates'; fetchTemplates()" id="nav-templates-btn">
<span>📝</span> <span x-text="lang === 'ar' ? 'قوالب الرسائل' : 'Message Templates'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'campaigns' }" @click="activeDashboardTab = 'campaigns'; fetchCampaigns()" id="nav-campaigns-btn">
<span>📣</span> <span x-text="lang === 'ar' ? 'الحملات التسويقية' : 'Marketing Campaigns'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'chatbot' }" @click="activeDashboardTab = 'chatbot'; fetchChatbotSettings()" id="nav-chatbot-btn">
<span>🤖</span> <span x-text="lang === 'ar' ? 'روبوت الذكاء الاصطناعي' : 'AI Chatbot Settings'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'integrations' }" @click="activeDashboardTab = 'integrations'; fetchEndpoints(); fetchSallaStatus()" id="nav-integrations-btn">
<span>🔌</span> <span x-text="lang === 'ar' ? 'الربط البرمجي والمنصات (Integrations)' : 'API & Platform Integrations'"></span>
</button>
</div>
<!-- Right Dashboard Panels -->
<div style="flex: 1;">
<!-- Global Dashboard Banner -->
<template x-if="dashboardSuccess">
<div class="banner banner-success" style="margin-bottom: 1.5rem;" id="dashboard-success-banner">
<span x-text="dashboardSuccess"></span>
<button @click="dashboardSuccess = ''" style="background: none; border: none; color: inherit; font-size: 1.2rem; cursor: pointer; margin-left: auto; margin-right: 0;" id="dashboard-success-close-btn">&times;</button>
</div>
</template>
<template x-if="dashboardError">
<div class="banner banner-danger" style="margin-bottom: 1.5rem;" id="dashboard-error-banner">
<span x-text="dashboardError"></span>
<button @click="dashboardError = ''" style="background: none; border: none; color: inherit; font-size: 1.2rem; cursor: pointer; margin-left: auto; margin-right: 0;" id="dashboard-error-close-btn">&times;</button>
</div>
</template>
<!-- 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;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<h2 style="font-size: 1.4rem; margin: 0;" x-text="lang === 'ar' ? 'ربط تطبيق نبيه بمشروعك (API Integrations)' : 'API Endpoints Integration'"></h2>
<button class="btn btn-primary" style="width: auto;" @click="endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', api_key: '' }; showAddEndpointModal = true" id="add-endpoint-btn">
<span x-text="lang === 'ar' ? '+ إضافة ربط برمجي' : '+ Add API Integration'"></span>
</button>
</div>
<p class="text-muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;" x-text="lang === 'ar' ? 'قم بتهيئة واجهات برمجة التطبيقات الخارجية (Web APIs) للربط البرمجي بمشروعك. يمكن للروبوت جلب الملفات التعريفية للمستخدمين أو التحقق من تفاصيل الدفع ديناميكيًا.' : 'Configure external web APIs for multi-tenant integrations. The chatbot can fetch user profiles or verify payment details dynamically.'">
</p>
<!-- Salla Integration Section -->
<div style="background: rgba(0, 178, 137, 0.05); border: 1px solid rgba(0, 178, 137, 0.15); border-radius: 12px; padding: 1.5rem; margin-bottom: 2rem; box-shadow: 0 4px 20px rgba(0, 178, 137, 0.05);">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<div style="display: flex; align-items: center; gap: 1rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse; text-align: right;' : ''">
<div style="background: #00b289; width: 48px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 15px rgba(0, 178, 137, 0.4);">
<span style="font-size: 1.5rem;">🛍️</span>
</div>
<div>
<h3 style="font-size: 1.2rem; margin: 0; display: flex; align-items: center; gap: 0.5rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<span x-text="lang === 'ar' ? 'ربط متجر سلة (Salla)' : 'Salla Store Integration'"></span>
<template x-if="sallaStatus && sallaStatus.connected">
<span class="status-badge badge-connected" style="margin: 0; padding: 0.15rem 0.5rem; font-size: 0.7rem;" x-text="lang === 'ar' ? 'متصل' : 'Connected'"></span>
</template>
<template x-if="!sallaStatus || !sallaStatus.connected">
<span class="status-badge badge-disconnected" style="margin: 0; padding: 0.15rem 0.5rem; font-size: 0.7rem;" x-text="lang === 'ar' ? 'غير متصل' : 'Disconnected'"></span>
</template>
</h3>
<p class="text-muted" style="margin: 0.25rem 0 0 0; font-size: 0.85rem;" x-text="lang === 'ar' ? 'قم بربط متجر سلة الخاص بك لتفعيل الاستعلام التلقائي عن الطلبات وتتبع الشحنات وإرسال تنبيهات التحديثات للعملاء عبر الواتساب.' : 'Connect your Salla store to enable automatic order tracking and send customer updates via WhatsApp using Gemini AI.'"></p>
</div>
</div>
<div style="display: flex; gap: 0.75rem; align-items: center;">
<template x-if="sallaLoading">
<span class="spinner"></span>
</template>
<template x-if="!sallaLoading && (!sallaStatus || !sallaStatus.connected)">
<button @click="connectSalla()" class="btn" style="background: linear-gradient(135deg, #00b289 0%, #009673 100%); color: #fff; width: auto; font-size: 0.9rem; padding: 0.6rem 1.2rem; box-shadow: 0 4px 12px rgba(0, 178, 137, 0.3);" id="connect-salla-btn">
<span x-text="lang === 'ar' ? 'ربط المتجر الآن' : 'Connect Store'"></span>
</button>
</template>
<template x-if="!sallaLoading && sallaStatus && sallaStatus.connected">
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<div style="text-align: right;" :style="lang === 'ar' ? 'text-align: right;' : 'text-align: left;'">
<span style="font-size: 0.8rem; color: var(--text-secondary);" x-text="lang === 'ar' ? 'المتجر المرتبط:' : 'Connected Store:'"></span>
<strong style="display: block; font-size: 0.95rem; color: #fff;" x-text="sallaStatus.store_name"></strong>
</div>
<button @click="disconnectSalla()" class="btn btn-danger" style="width: auto; font-size: 0.9rem; padding: 0.6rem 1.2rem;" id="disconnect-salla-btn">
<span x-text="lang === 'ar' ? 'إلغاء الربط' : 'Disconnect'"></span>
</button>
</div>
</template>
</div>
</div>
</div>
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th x-text="lang === 'ar' ? 'اسم الربط' : 'Integration Name'"></th>
<th x-text="lang === 'ar' ? 'نوع الإجراء' : 'Action Type'"></th>
<th x-text="lang === 'ar' ? 'رابط نقطة النهاية (Endpoint URL)' : 'Endpoint URL'"></th>
<th x-text="lang === 'ar' ? 'الوصف' : 'Description'"></th>
<th style="width: 150px; text-align: center;" x-text="lang === 'ar' ? 'الخيارات' : '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' ? (lang === 'ar' ? 'تأكيد الدفع' : 'Verify Payment') : (endpoint.action_type === 'fetch_user_info' ? (lang === 'ar' ? 'جلب بيانات العميل' : 'Fetch User Info') : endpoint.action_type)"></span>
</td>
<td style="font-family: monospace; font-size: 0.85rem;" x-text="endpoint.endpoint_url"></td>
<td x-text="endpoint.description || (lang === 'ar' ? 'لا يوجد وصف.' : 'No description provided.')"></td>
<td style="text-align: center; display: flex; gap: 0.5rem; justify-content: center; align-items: center;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<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" x-text="lang === 'ar' ? 'تعديل' : '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" x-text="lang === 'ar' ? 'حذف' : 'Delete'"></button>
</td>
</tr>
</template>
</tbody>
</table>
<template x-if="endpoints.length === 0">
<div class="empty-state" id="empty-endpoints-state" x-text="lang === 'ar' ? 'لم يتم تكوين نقاط نهاية واجهة برمجة تطبيقات (API Endpoints) بعد.' : '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" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<h3 class="modal-title" x-text="endpointForm.id ? (lang === 'ar' ? 'تعديل ربط تطبيق نبيه بمشروعك' : 'Edit API Endpoint Integration') : (lang === 'ar' ? 'إضافة ربط تطبيق نبيه بمشروعك' : '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" x-text="lang === 'ar' ? 'اسم الربط' : '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" x-text="lang === 'ar' ? 'رابط نقطة النهاية (Endpoint URL)' : '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" x-text="lang === 'ar' ? 'نوع الإجراء (Action Type)' : 'Action Type'"></label>
<input type="text" class="form-input" x-model="endpointForm.action_type" required placeholder="e.g. verify_payment" id="add-endpoint-action-type">
<!-- Shortcuts/Hint Tags -->
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem; flex-wrap: wrap;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<button type="button" class="btn btn-secondary" style="width: auto; padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 4px;" @click="endpointForm.action_type = 'verify_payment'">
verify_payment (تحليل وتأكيد الدفع)
</button>
<button type="button" class="btn btn-secondary" style="width: auto; padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 4px;" @click="endpointForm.action_type = 'fetch_user_info'">
fetch_user_info (جلب معلومات العميل)
</button>
</div>
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;" x-text="lang === 'ar' ? 'يمكنك كتابة نوع إجراء مخصص هنا لتسهيل ربط النظام أو الضغط على الاختصارات بالأعلى.' : 'You can type a custom action type here or click the shortcuts above.'"></span>
</div>
<div class="form-group">
<label class="form-label" x-text="lang === 'ar' ? 'الوصف' : '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" x-text="lang === 'ar' ? 'مفتاح واجهة برمجة التطبيقات (API Key)' : 'API Key'"></label>
<input type="password" class="form-input" x-model="endpointForm.api_key" placeholder="e.g. key_xxxxxxxxxxxxxxxxx" id="add-endpoint-api-key">
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;" x-text="lang === 'ar' ? 'سيتم إرسال هذا المفتاح في ترويسات X-API-Key و Authorization: Bearer عند استدعاء الخادم.' : 'This key will be automatically sent in X-API-Key and Authorization: Bearer headers for outbound server-to-server calls.'"></span>
</div>
</div>
<div class="modal-footer" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showAddEndpointModal = false" x-text="lang === 'ar' ? 'إلغاء' : 'Cancel'"></button>
<button type="submit" class="btn btn-primary" style="width: auto;" :disabled="actionLoading">
<span x-show="!actionLoading" x-text="endpointForm.id ? (lang === 'ar' ? 'حفظ التعديلات' : 'Save Changes') : (lang === 'ar' ? 'إنشاء الربط' : '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 {
// Multi-Language State
lang: localStorage.getItem('nabeh_lang') || 'ar',
// 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: '',
api_key: ''
},
endpoints: [],
// Salla Integration States
sallaStatus: null,
sallaLoading: false,
dashboardSuccess: '',
dashboardError: '',
// 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
toggleLang() {
this.lang = this.lang === 'ar' ? 'en' : 'ar';
localStorage.setItem('nabeh_lang', this.lang);
},
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 = [];
this.sallaStatus = null;
this.dashboardSuccess = '';
this.dashboardError = '';
},
initializeDashboard() {
this.fetchWhatsappStatus();
this.fetchSallaStatus();
// Set up persistent background status check
this.startPolling();
// Detect Salla success connect
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('salla_connect') === 'success') {
this.dashboardSuccess = this.lang === 'ar'
? 'تم ربط متجر سلة بنجاح!'
: 'Salla store connected successfully!';
window.history.replaceState({}, document.title, window.location.pathname);
} else if (urlParams.get('salla_connect') === 'error') {
const reason = urlParams.get('reason') || '';
this.dashboardError = this.lang === 'ar'
? 'فشل ربط متجر سلة: ' + reason
: 'Salla connection failed: ' + reason;
window.history.replaceState({}, document.title, window.location.pathname);
}
},
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 {
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: '', api_key: '' };
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 || '',
api_key: endpoint.api_key || ''
};
this.showAddEndpointModal = true;
},
async deleteEndpoint(id) {
const confirmMsg = this.lang === 'ar'
? 'هل أنت متأكد من رغبتك في حذف ربط نقطة النهاية هذا؟'
: 'Are you sure you want to delete this integration endpoint?';
if (!confirm(confirmMsg)) 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;
}
},
// Salla Integration Methods
async fetchSallaStatus() {
if (!this.token) return;
this.sallaLoading = true;
try {
const response = await fetch('/api/integrations/salla/status', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok && data.status === 'success') {
this.sallaStatus = data;
} else {
this.sallaStatus = { connected: false };
}
} catch (err) {
console.error('Error fetching Salla status:', err);
this.sallaStatus = { connected: false };
} finally {
this.sallaLoading = false;
}
},
connectSalla() {
if (!this.token) return;
window.location.href = `/api/integrations/salla/auth?token=${encodeURIComponent(this.token)}`;
},
async disconnectSalla() {
const confirmMsg = this.lang === 'ar'
? 'هل أنت متأكد من رغبتك في إلغاء ربط متجر سلة؟'
: 'Are you sure you want to disconnect your Salla store?';
if (!confirm(confirmMsg)) return;
this.sallaLoading = true;
try {
const response = await fetch('/api/integrations/salla/disconnect', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok && data.status === 'success') {
this.dashboardSuccess = this.lang === 'ar'
? 'تم إلغاء ربط متجر سلة بنجاح.'
: 'Salla store disconnected successfully.';
await this.fetchSallaStatus();
} else {
this.dashboardError = data.message || 'Failed to disconnect Salla integration.';
}
} catch (err) {
console.error('Error disconnecting Salla:', err);
this.dashboardError = 'Failed to disconnect Salla integration.';
} finally {
this.sallaLoading = 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>