3142 lines
187 KiB
HTML
3142 lines
187 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">
|
|
<!-- Super Admin Dashboard -->
|
|
<button class="nav-item" x-show="user?.is_super_admin" :class="{ 'active': activeDashboardTab === 'super_admin' }" @click="activeDashboardTab = 'super_admin'; fetchSuperAdminStats()" id="nav-superadmin-btn">
|
|
<span class="nav-icon">👑</span>
|
|
<span class="nav-text" x-text="lang === 'ar' ? 'لوحة المشرف العام' : 'Super Admin'"></span>
|
|
</button>
|
|
|
|
<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 === 'billing' }" @click="activeDashboardTab = 'billing'; fetchPlans()" id="nav-billing-btn">
|
|
<span>💳</span> <span x-text="lang === 'ar' ? 'الباقات والاشتراكات' : 'Billing & Plans'"></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(); fetchWooCommerceStatus()" id="nav-integrations-btn">
|
|
<span>🔌</span> <span x-text="lang === 'ar' ? 'الربط البرمجي والمنصات (Integrations)' : 'API & Platform Integrations'"></span>
|
|
</button>
|
|
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'staff' }" @click="activeDashboardTab = 'staff'; fetchStaff(); fetchWhatsappSessions()" id="nav-staff-btn">
|
|
<span>👥</span> <span x-text="lang === 'ar' ? 'الموظفين والوكلاء' : 'CS Agents & Team'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Right Dashboard Panels -->
|
|
<div style="flex: 1;">
|
|
<template x-if="user?.subscription_status === 'trialing' || (user?.subscription_status === 'active' && user?.trial_days_left < 14)">
|
|
<div class="banner banner-warning" style="margin-bottom: 1.5rem;">
|
|
<span x-text="lang === 'ar' ? 'متبقي لك ' + user.trial_days_left + ' أيام من الفترة التجريبية. اشترك الآن لضمان استمرار الخدمة.' : 'You have ' + user.trial_days_left + ' days left in your trial. Subscribe now to ensure service continuity.'"></span>
|
|
</div>
|
|
</template>
|
|
<!-- 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">×</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">×</button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Panel: WhatsApp Connection -->
|
|
<!-- Super Admin Panel -->
|
|
<div class="panel" x-show="activeDashboardTab === 'super_admin'" id="panel-superadmin">
|
|
<h2 x-text="lang === 'ar' ? 'لوحة المشرف العام' : 'Super Admin Dashboard'"></h2>
|
|
<p class="text-muted" x-text="lang === 'ar' ? 'إدارة الشركات والباقات.' : 'Manage tenants and subscriptions.'"></p>
|
|
|
|
<div class="dashboard-stats" style="margin-top: 1rem;" x-show="superAdminStats">
|
|
<div class="stat-card">
|
|
<div class="stat-title" x-text="lang === 'ar' ? 'إجمالي الشركات' : 'Total Companies'"></div>
|
|
<div class="stat-value" x-text="superAdminStats?.total_companies"></div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-title" x-text="lang === 'ar' ? 'أرقام الواتساب المربوطة' : 'Connected WhatsApps'"></div>
|
|
<div class="stat-value" x-text="superAdminStats?.connected_sessions + ' / ' + superAdminStats?.total_sessions"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 2rem; margin-bottom: 2rem;" x-show="superAdminPending?.length > 0">
|
|
<h3 style="margin-bottom: 1rem;"><span style="color: var(--warning-color);">⏳</span> <span x-text="lang === 'ar' ? 'طلبات الدفع بانتظار الموافقة' : 'Pending Approvals'"></span></h3>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th x-text="lang === 'ar' ? 'معرف الشركة' : 'Company ID'"></th>
|
|
<th x-text="lang === 'ar' ? 'اسم الشركة' : 'Company Name'"></th>
|
|
<th x-text="lang === 'ar' ? 'الباقة المطلوبة' : 'Requested Plan'"></th>
|
|
<th x-text="lang === 'ar' ? 'طريقة الدفع' : 'Payment Method'"></th>
|
|
<th x-text="lang === 'ar' ? 'رقم الحوالة' : 'Receipt Reference'"></th>
|
|
<th x-text="lang === 'ar' ? 'الإجراء' : 'Action'"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="req in superAdminPending" :key="req.company_id">
|
|
<tr>
|
|
<td x-text="'#' + req.id"></td>
|
|
<td x-text="req.name"></td>
|
|
<td><span class="badge badge-info" x-text="req.plan_name"></span></td>
|
|
<td x-text="req.payment_method?.toUpperCase()"></td>
|
|
<td x-text="req.receipt_reference"></td>
|
|
<td>
|
|
<button class="btn btn-primary btn-sm" @click="approveBilling(req.id)" x-text="lang === 'ar' ? 'موافقة وتفعيل' : 'Approve & Activate'"></button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 style="margin-top:2rem;" x-text="lang === 'ar' ? 'قائمة الشركات' : 'Companies List'"></h3>
|
|
<div class="data-table" style="overflow-x: auto;">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th x-text="lang === 'ar' ? 'اسم الشركة' : 'Company Name'"></th>
|
|
<th x-text="lang === 'ar' ? 'الباقة الحالية' : 'Current Plan'"></th>
|
|
<th x-text="lang === 'ar' ? 'استهلاك الرسائل' : 'Request Usage'"></th>
|
|
<th x-text="lang === 'ar' ? 'حالة الواتساب' : 'WhatsApp Status'"></th>
|
|
<th x-text="lang === 'ar' ? 'الإجراءات' : 'Actions'"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="company in superAdminCompanies" :key="company.id">
|
|
<tr>
|
|
<td x-text="company.id"></td>
|
|
<td x-text="company.name"></td>
|
|
<td x-text="company.plan_name ? company.plan_name : 'No Plan'"></td>
|
|
<td x-text="company.request_usage"></td>
|
|
<td x-text="company.active_sessions + ' / ' + company.sessions_count"></td>
|
|
<td>
|
|
<select class="form-input" style="padding: 0.2rem; font-size: 0.85rem;" @change="changeCompanyPlan(company.id, $event.target.value)">
|
|
<option value="" disabled selected x-text="lang === 'ar' ? 'تغيير الباقة...' : 'Change Plan...'"></option>
|
|
<template x-for="plan in superAdminPlans" :key="plan.id">
|
|
<option :value="plan.id" x-text="plan.name"></option>
|
|
</template>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel" x-show="activeDashboardTab === 'whatsapp'" id="panel-whatsapp">
|
|
<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' ? 'إدارة قنوات اتصال واتساب' : 'WhatsApp Session Management'"></h2>
|
|
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
|
<span class="badge badge-warning" x-show="whatsappSessions.length >= whatsappMaxSessions" x-text="lang === 'ar' ? 'تم الوصول للحد الأقصى (' + whatsappSessions.length + '/' + whatsappMaxSessions + ')' : 'Limit Reached (' + whatsappSessions.length + '/' + whatsappMaxSessions + ')'"></span>
|
|
<span class="badge badge-success" x-show="whatsappSessions.length < whatsappMaxSessions" x-text="whatsappSessions.length + ' / ' + whatsappMaxSessions + (lang === 'ar' ? ' أرقام' : ' Numbers')"></span>
|
|
<input type="text" x-model="newSessionName" class="form-input" :placeholder="lang === 'ar' ? 'اسم الرقم (مثال: الدعم)' : 'Session Name (e.g. Sales)'" style="width: 250px;" id="new-session-name-input">
|
|
<button @click="createWhatsappSession()" class="btn btn-primary" style="padding: 0.5rem 1rem; font-size: 0.85rem;" id="btn-create-session" :disabled="actionLoading || whatsappSessions.length >= whatsappMaxSessions">
|
|
<span x-text="lang === 'ar' ? '+ إضافة خط جديد' : '+ Add New Line'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;" x-text="lang === 'ar' ? 'قم بإضافة وإدارة أرقام واتساب متعددة لشركتك طبقاً لباقة اشتراكك. يمكنك ربط كل موظف بخط محدد لمتابعة المحادثات.' : 'Add and manage multiple WhatsApp connections. You can assign customer service agents to specific phone lines based on your plan features.'"></p>
|
|
|
|
<!-- Sessions Grid Layout -->
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
|
<template x-for="session in whatsappSessions" :key="session.id">
|
|
<div class="status-box" style="margin: 0; padding: 1.5rem; display: flex; flex-direction: column; justify-content: space-between; height: 100%; border: 1px solid var(--card-border); background: var(--card-bg); border-radius: 16px;">
|
|
<div>
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
|
|
<div>
|
|
<h3 style="font-size: 1.1rem; font-weight: 700;" x-text="session.name"></h3>
|
|
<span style="font-family: monospace; font-size: 0.8rem; color: var(--text-muted);" x-text="session.session_key"></span>
|
|
</div>
|
|
<div class="status-badge" :class="{
|
|
'badge-disconnected': session.status === 'disconnected',
|
|
'badge-connecting': session.status === 'connecting',
|
|
'badge-waiting_qr': session.status === 'waiting_qr',
|
|
'badge-connected': session.status === 'connected'
|
|
}" style="margin: 0; font-size: 0.75rem; padding: 0.2rem 0.5rem;">
|
|
<span x-text="session.status"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<p style="font-size: 0.9rem; margin-bottom: 1.5rem; color: var(--text-muted);" :style="lang === 'ar' ? 'text-align: right;' : 'text-align: left;'">
|
|
<template x-if="session.phone">
|
|
<span>📞 <strong x-text="session.phone" style="color: var(--text-main);"></strong></span>
|
|
</template>
|
|
<template x-if="!session.phone">
|
|
<span x-text="lang === 'ar' ? 'الخط غير مرتبط برقم هاتف بعد.' : 'No phone linked yet. Scan QR code.'"></span>
|
|
</template>
|
|
</p>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 0.5rem; justify-content: flex-end; flex-wrap: wrap;">
|
|
<!-- Connect/Scan QR Action -->
|
|
<template x-if="session.status === 'disconnected' || session.status === 'waiting_qr' || session.status === 'connecting'">
|
|
<button @click="connectWhatsapp(session.id)" class="btn btn-primary" style="padding: 0.4rem 0.8rem; font-size: 0.8rem;" :disabled="actionLoading">
|
|
<span x-text="lang === 'ar' ? 'ربط / مسح رمز QR' : 'Link / Scan QR'"></span>
|
|
</button>
|
|
</template>
|
|
|
|
<!-- Disconnect Action -->
|
|
<template x-if="session.status === 'connected'">
|
|
<button @click="disconnectWhatsapp(session.id)" class="btn btn-glass" style="padding: 0.4rem 0.8rem; font-size: 0.8rem; color: var(--warning);" :disabled="actionLoading">
|
|
<span x-text="lang === 'ar' ? 'قطع الاتصال' : 'Disconnect'"></span>
|
|
</button>
|
|
</template>
|
|
|
|
<!-- Delete Action -->
|
|
<button @click="deleteWhatsappSession(session.id)" class="btn btn-danger" style="padding: 0.4rem 0.8rem; font-size: 0.8rem;" :disabled="actionLoading">
|
|
<span x-text="lang === 'ar' ? 'حذف' : 'Delete'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="whatsappSessions.length === 0">
|
|
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: var(--text-muted);" x-text="lang === 'ar' ? 'لا توجد قنوات واتساب نشطة. أضف خطاً جديداً أعلاه للبدء.' : 'No active WhatsApp channels configured. Create a session above to get started.'"></div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="grid-two" style="align-items: start;">
|
|
<!-- QR Display Card (Active selected session) -->
|
|
<div class="card" style="margin: 0;" x-show="whatsappSession && (whatsappSession.status === 'connecting' || whatsappSession.status === 'waiting_qr' || whatsappSession.status === 'connected')">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
|
|
<h3 style="font-size: 1.1rem; font-weight: 700;" x-text="(lang === 'ar' ? 'ربط الخط: ' : 'Linking Line: ') + (whatsappSession?.name || '')"></h3>
|
|
<button class="modal-close" style="font-size: 1.25rem;" @click="whatsappSession = null">×</button>
|
|
</div>
|
|
|
|
<div class="flex-center flex-column" style="background: rgba(10, 11, 20, 0.2); border: 1px solid var(--card-border); border-radius: 12px; padding: 2rem;">
|
|
<template x-if="whatsappSession?.status === 'connecting'">
|
|
<div class="text-center">
|
|
<div class="spinner spinner-large" style="margin-bottom: 1rem;"></div>
|
|
<p class="font-semibold" x-text="lang === 'ar' ? 'جاري الاتصال بالبوابة...' : 'Connecting to Gateway...'"></p>
|
|
<p class="text-muted" style="font-size: 0.8rem; margin-top: 0.25rem;" x-text="lang === 'ar' ? 'يرجى الانتظار لحين جلب حالة الخط وطلب الرمز من البوابة.' : 'Preparing session and requesting QR code stream.'"></p>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="whatsappSession?.status === 'waiting_qr'">
|
|
<div class="text-center">
|
|
<p class="font-semibold" x-text="lang === 'ar' ? 'امسح رمز الاستجابة السريعة (QR Code)' : 'Scan QR Code'"></p>
|
|
<p class="text-muted" style="font-size: 0.8rem; margin-top: 0.25rem; margin-bottom: 1rem;" x-text="lang === 'ar' ? 'افتح واتساب > الأجهزة المرتبطة > ربط جهاز' : 'Open WhatsApp > Linked Devices > Link a Device'"></p>
|
|
|
|
<div class="qr-wrapper" style="background: #ffffff; padding: 1rem; border-radius: 12px; display: inline-block;">
|
|
<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; margin-top: 1rem;" class="text-muted">
|
|
<div class="spinner" style="width: 14px; height: 14px; border-width: 2px;"></div>
|
|
<span x-text="lang === 'ar' ? 'بانتظار إتمام المصادقة من الهاتف...' : 'Waiting for connection handshake...'"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="whatsappSession?.status === 'connected'">
|
|
<div class="text-center" style="padding: 1.5rem 0;">
|
|
<div style="font-size: 3rem; color: var(--success); margin-bottom: 0.5rem; text-shadow: 0 0 20px rgba(16,185,129,0.3);">✓</div>
|
|
<p class="font-semibold" style="font-size: 1.1rem; color: var(--success);" x-text="lang === 'ar' ? 'الخط متصل بالكامل برقم الهاتف!' : 'Line fully linked and connected!'"></p>
|
|
<p class="text-muted" style="font-size: 0.85rem; margin-top: 0.25rem;" x-text="lang === 'ar' ? 'الرقم متصل ويمكنه الآن إرسال الحملات ورموز التحقق واستقبال الطلبات.' : 'This line is active and ready to deliver campaign broadcasts and verification OTPs.'"></p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- OTP Test Widget Card -->
|
|
<div class="card" style="margin: 0;">
|
|
<h3 style="font-size: 1.1rem; font-weight: 700; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
|
|
<span>🔑</span>
|
|
<span x-text="lang === 'ar' ? 'أداة اختبار إرسال رمز التحقق (OTP)' : 'OTP Deliverability Test Tool'"></span>
|
|
</h3>
|
|
|
|
<form @submit.prevent="sendOtpTest()">
|
|
<div class="form-group">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'خط الإرسال (WhatsApp Line)' : 'Sender Line (WhatsApp)'"></label>
|
|
<select x-model="otpSessionId" required>
|
|
<option value="" x-text="lang === 'ar' ? '-- اختر الخط --' : '-- Choose Line --'"></option>
|
|
<template x-for="session in whatsappSessions" :key="session.id">
|
|
<option :value="session.id" x-text="session.name + (session.phone ? ' (' + session.phone + ')' : ' - ' + session.status)"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'رقم الهاتف المستلم (مع رمز الدولة)' : 'Recipient Phone (with country code)'"></label>
|
|
<input type="text" x-model="otpPhone" class="form-input" required placeholder="966500000000" id="otp-test-recipient">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'نوع الرسالة' : 'OTP Type'"></label>
|
|
<select x-model="otpType">
|
|
<option value="image" x-text="lang === 'ar' ? 'صورة آمنة (Image CAPTCHA OTP)' : 'Secure Image (Anti-Bot OTP)'"></option>
|
|
<option value="text" x-text="lang === 'ar' ? 'رسالة نصية على الواتساب' : 'Text Message'"></option>
|
|
<option value="voice" x-text="lang === 'ar' ? 'رسالة صوتية (Voice Note OTP)' : 'Voice Note (Google TTS)'"></option>
|
|
</select>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1rem;" :disabled="actionLoading" id="btn-send-otp-test">
|
|
<span x-show="!actionLoading" x-text="lang === 'ar' ? 'إرسال رمز التحقق الآن' : 'Deliver OTP Code'"></span>
|
|
<span x-show="actionLoading" class="spinner"></span>
|
|
</button>
|
|
</form>
|
|
|
|
<template x-if="otpStatusMsg">
|
|
<div class="banner" :class="otpErrorCode ? 'banner-danger' : 'banner-success'" style="margin-top: 1rem; font-size: 0.85rem;" id="otp-test-banner">
|
|
<span x-text="otpStatusMsg"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Panel: Billing & Plans -->
|
|
<div class="panel" x-show="activeDashboardTab === 'billing'" id="panel-billing">
|
|
<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' ? 'الباقات والاشتراكات' : 'Billing & Plans'"></h2>
|
|
</div>
|
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem;" :style="lang === 'ar' ? 'direction: rtl;' : ''">
|
|
<template x-for="plan in availablePlans" :key="plan.id">
|
|
<div class="card" style="border: 2px solid transparent; transition: 0.3s; padding: 2rem; text-align: center;" :style="userPlanId == plan.id ? 'border-color: var(--primary-accent);' : ''">
|
|
<h3 style="font-size: 1.5rem; margin-bottom: 0.5rem;" x-text="plan.name"></h3>
|
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-accent); margin-bottom: 1rem;">
|
|
$<span x-text="plan.price"></span> <span style="font-size: 1rem; color: var(--text-muted);">/ <span x-text="lang === 'ar' ? 'شهر' : 'mo'"></span></span>
|
|
</div>
|
|
<ul style="list-style: none; padding: 0; margin-bottom: 1.5rem; text-align: start; line-height: 1.8;">
|
|
<li><span style="margin-right: 0.5rem;">✅</span> <span x-text="plan.max_sessions"></span> <span x-text="lang === 'ar' ? 'أرقام واتساب' : 'WhatsApp Numbers'"></span></li>
|
|
<li><span style="margin-right: 0.5rem;">✅</span> <span x-text="plan.max_requests"></span> <span x-text="lang === 'ar' ? 'رسالة نصية' : 'Text Messages'"></span></li>
|
|
<li><span style="margin-right: 0.5rem;">✅</span> <span x-text="plan.max_voice_requests"></span> <span x-text="lang === 'ar' ? 'رسالة صوتية' : 'Voice Notes'"></span></li>
|
|
</ul>
|
|
<button class="btn btn-primary" style="width: 100%;" @click="openCheckoutModal(plan)" :disabled="userPlanId == plan.id" x-text="userPlanId == plan.id ? (lang === 'ar' ? 'باقتك الحالية' : 'Current Plan') : (lang === 'ar' ? 'ترقية الآن' : 'Upgrade Now')"></button>
|
|
</div>
|
|
</template>
|
|
</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('intaleq')">قالب خدمة عملاء انطلق (سوري)</button>
|
|
<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>
|
|
|
|
<!-- WooCommerce Integration Section -->
|
|
<div style="background: rgba(99, 102, 241, 0.05); border: 1px solid rgba(99, 102, 241, 0.15); border-radius: 12px; padding: 1.5rem; margin-bottom: 2rem; box-shadow: 0 4px 20px rgba(99, 102, 241, 0.05);">
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 1.5rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
|
|
<div style="display: flex; align-items: center; gap: 1rem; flex: 1;" :style="lang === 'ar' ? 'flex-direction: row-reverse; text-align: right;' : ''">
|
|
<div style="background: var(--primary); width: 48px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 15px rgba(99, 102, 241, 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' ? 'ربط متجر ووكومرس (WooCommerce)' : 'WooCommerce Store Integration'"></span>
|
|
<template x-if="woocommerceStatus && woocommerceStatus.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="!woocommerceStatus || !woocommerceStatus.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' ? 'قم بربط متجر WooCommerce لإرسال إشعارات تغيير حالة الطلبات للعملاء تلقائيًا عبر الواتساب.' : 'Link your WooCommerce store to trigger automated customer notifications via WhatsApp on order events.'"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 0.75rem; align-items: center;">
|
|
<template x-if="woocommerceLoading">
|
|
<span class="spinner"></span>
|
|
</template>
|
|
<template x-if="!woocommerceLoading && woocommerceStatus && woocommerceStatus.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 URL:'"></span>
|
|
<strong style="display: block; font-size: 0.95rem; color: #fff;" x-text="woocommerceStatus.store_url"></strong>
|
|
</div>
|
|
<button @click="disconnectWooCommerce()" class="btn btn-danger" style="width: auto; font-size: 0.9rem; padding: 0.6rem 1.2rem;" id="disconnect-woo-btn">
|
|
<span x-text="lang === 'ar' ? 'إلغاء الربط' : 'Disconnect'"></span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- WooCommerce Connect Form -->
|
|
<template x-if="!woocommerceStatus || !woocommerceStatus.connected">
|
|
<form @submit.prevent="connectWooCommerce()" style="margin-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 1.5rem;">
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
|
|
<div class="form-group" style="margin: 0;">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'رابط المتجر (Store URL)' : 'Store URL'"></label>
|
|
<input type="url" class="form-input" x-model="wooForm.store_url" required placeholder="https://my-store.com">
|
|
</div>
|
|
<div class="form-group" style="margin: 0;">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'Consumer Key (ck_...)' : 'Consumer Key (ck_...)'"></label>
|
|
<input type="text" class="form-input" x-model="wooForm.consumer_key" required placeholder="ck_...">
|
|
</div>
|
|
<div class="form-group" style="margin: 0;">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'Consumer Secret (cs_...)' : 'Consumer Secret (cs_...)'"></label>
|
|
<input type="password" class="form-input" x-model="wooForm.consumer_secret" required placeholder="cs_...">
|
|
</div>
|
|
<div class="form-group" style="margin: 0;">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'Webhook Secret (اختياري)' : 'Webhook Secret (Optional)'"></label>
|
|
<input type="text" class="form-input" x-model="wooForm.webhook_secret" placeholder="Secret code to secure webhook signature">
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" style="width: auto;" :disabled="woocommerceLoading" id="connect-woo-btn">
|
|
<span x-text="lang === 'ar' ? 'ربط ووكومرس وتفعيل الاشعارات' : 'Link WooCommerce Store'"></span>
|
|
</button>
|
|
</form>
|
|
</template>
|
|
|
|
<!-- WooCommerce Connected Details -->
|
|
<template x-if="woocommerceStatus && woocommerceStatus.connected">
|
|
<div style="margin-top: 1rem; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 1rem; font-size: 0.85rem;">
|
|
<div style="background: rgba(255,255,255,0.02); border: 1px dashed var(--card-border-hover); border-radius: 8px; padding: 1rem; font-family: monospace;">
|
|
<p style="font-weight: 600; margin-bottom: 0.25rem; color: var(--text-main);" x-text="lang === 'ar' ? 'رابط الـ Webhook الخاص بمتجرك:' : 'Delivery URL for WooCommerce Webhook:'"></p>
|
|
<p style="word-break: break-all; color: var(--secondary);" x-text="woocommerceStatus.webhook_url"></p>
|
|
<p style="margin-top: 0.5rem; color: var(--text-muted); font-size: 0.8rem;" x-text="lang === 'ar' ? 'قم بإنشاء Webhooks في ووكومرس للأحداث (Order Created & Order Updated) والصق هذا الرابط هناك.' : 'Create Order Created and Order Updated webhooks in WooCommerce settings using this delivery URL.'"></p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</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>
|
|
|
|
<!-- Panel: Customer Service Team & Staff -->
|
|
<div class="panel" x-show="activeDashboardTab === 'staff'" id="panel-staff">
|
|
<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' ? 'فريق خدمة العملاء والوكلاء (Staff)' : 'Customer Service Agents & Staff'"></h2>
|
|
<button class="btn btn-primary" style="width: auto;" @click="staffForm = { name: '', email: '', password: '', whatsapp_session_id: '' }; showAddStaffModal = true" id="add-agent-btn">
|
|
<span x-text="lang === 'ar' ? '+ إضافة موظف جديد' : '+ Add CS Agent'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<p class="text-muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;" x-text="lang === 'ar' ? 'قم بإضافة موظفي خدمة العملاء وتعيين كل منهم لرقم واتساب محدد. يستطيع كل موظف قراءة والرد على رسائل الرقم المخصص له فقط.' : 'Add agents to your team and bind them to specific WhatsApp lines. Each staff member can only view and manage chats for their assigned numbers.'"></p>
|
|
|
|
<div class="data-table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th x-text="lang === 'ar' ? 'الاسم' : 'Agent Name'"></th>
|
|
<th x-text="lang === 'ar' ? 'البريد الإلكتروني' : 'Email Address'"></th>
|
|
<th x-text="lang === 'ar' ? 'رقم الواتساب المعين' : 'Assigned WhatsApp Line'"></th>
|
|
<th style="width: 250px; text-align: center;" x-text="lang === 'ar' ? 'خيارات وتغيير التعيين' : 'Management & Assign'"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="agent in staff" :key="agent.id">
|
|
<tr>
|
|
<td class="font-semibold" x-text="agent.name"></td>
|
|
<td style="font-family: monospace; font-size: 0.85rem;" x-text="agent.email"></td>
|
|
<td>
|
|
<span class="badge badge-primary" x-show="agent.whatsapp_session_id" x-text="agent.session_name + (agent.session_phone ? ' (' + agent.session_phone + ')' : '')"></span>
|
|
<span class="badge badge-danger" x-show="!agent.whatsapp_session_id" x-text="lang === 'ar' ? 'غير معين' : 'Unassigned'"></span>
|
|
</td>
|
|
<td style="text-align: center; display: flex; gap: 0.5rem; justify-content: center; align-items: center;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
|
|
<!-- Assign Session Select -->
|
|
<select style="font-size: 0.8rem; padding: 0.3rem 0.5rem; width: auto;" :value="agent.whatsapp_session_id || ''" @change="assignSessionToStaff(agent.id, $event.target.value)">
|
|
<option value="" x-text="lang === 'ar' ? '-- بدون تعيين --' : '-- Unassigned --'"></option>
|
|
<template x-for="session in whatsappSessions" :key="session.id">
|
|
<option :value="session.id" x-text="session.name + (session.phone ? ' (' + session.phone + ')' : '')"></option>
|
|
</template>
|
|
</select>
|
|
<button class="btn btn-danger" style="width: auto; padding: 0.3rem 0.6rem; font-size: 0.8rem; margin: 0;" @click="deleteStaff(agent.id)">
|
|
<span x-text="lang === 'ar' ? 'حذف' : 'Delete'"></span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
<template x-if="staff.length === 0">
|
|
<div class="empty-state" id="empty-staff-state" x-text="lang === 'ar' ? 'لم تقم بإضافة موظفي خدمة عملاء بعد.' : 'No customer service agents added yet.'"></div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Checkout -->
|
|
<div class="modal-overlay" x-show="showCheckoutModal" id="modal-checkout" style="display: none;">
|
|
<div class="modal-card">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title" x-text="lang === 'ar' ? 'إتمام عملية الدفع' : 'Complete Checkout'"></h3>
|
|
<button class="modal-close" @click="showCheckoutModal = false">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p x-text="lang === 'ar' ? 'أنت تقوم بترقية حسابك إلى باقة: ' + selectedCheckoutPlan?.name : 'You are upgrading to: ' + selectedCheckoutPlan?.name"></p>
|
|
<p style="font-weight: bold; font-size: 1.2rem; margin-bottom: 1rem;">
|
|
<span x-text="lang === 'ar' ? 'المبلغ المطلوب: ' : 'Total Amount: '"></span> $<span x-text="selectedCheckoutPlan?.price"></span>
|
|
</p>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'طريقة الدفع' : 'Payment Method'"></label>
|
|
<select x-model="checkoutPaymentMethod" class="form-input">
|
|
<option value="paymob" x-text="lang === 'ar' ? 'البطاقة الائتمانية (Paymob)' : 'Credit Card (Paymob)'"></option>
|
|
<option value="cliq" x-text="lang === 'ar' ? 'كليك (CliQ) - الأردن' : 'CliQ Transfer (Jordan)'"></option>
|
|
<option value="binance" x-text="lang === 'ar' ? 'بينانس (Binance Pay)' : 'Crypto (Binance)'"></option>
|
|
</select>
|
|
</div>
|
|
|
|
<template x-if="checkoutPaymentMethod !== 'paymob'">
|
|
<div class="form-group">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'رقم الحوالة / Transaction ID' : 'Receipt Reference / TXID'"></label>
|
|
<input type="text" x-model="checkoutReceipt" class="form-input" :placeholder="lang === 'ar' ? 'أدخل رقم الحوالة هنا' : 'Enter reference here'">
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" @click="showCheckoutModal = false" x-text="lang === 'ar' ? 'إلغاء' : 'Cancel'"></button>
|
|
<button class="btn btn-primary" @click="submitCheckout()" :disabled="actionLoading">
|
|
<span x-show="!actionLoading" x-text="lang === 'ar' ? 'تأكيد الدفع' : 'Confirm Payment'"></span>
|
|
<span x-show="actionLoading" class="spinner"></span>
|
|
</button>
|
|
</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">×</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 Staff / Agent -->
|
|
<div class="modal-overlay" x-show="showAddStaffModal" id="modal-add-staff" style="display: none;">
|
|
<div class="modal-card">
|
|
<div class="modal-header" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
|
|
<h3 class="modal-title" x-text="lang === 'ar' ? 'إضافة موظف خدمة عملاء جديد' : 'Add Customer Service Agent'"></h3>
|
|
<button class="modal-close" @click="showAddStaffModal = false">×</button>
|
|
</div>
|
|
<form @submit.prevent="submitAddStaff()" id="form-add-staff">
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'اسم الموظف' : 'Agent Name'"></label>
|
|
<input type="text" class="form-input" x-model="staffForm.name" required placeholder="Ali Ahmed">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'البريد الإلكتروني' : 'Email Address'"></label>
|
|
<input type="email" class="form-input" x-model="staffForm.email" required placeholder="ali@example.com">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'كلمة المرور' : 'Password'"></label>
|
|
<input type="password" class="form-input" x-model="staffForm.password" required placeholder="******">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" x-text="lang === 'ar' ? 'تعيين خط واتساب' : 'Assign WhatsApp Line'"></label>
|
|
<select x-model="staffForm.whatsapp_session_id">
|
|
<option value="" x-text="lang === 'ar' ? '-- بدون تعيين --' : '-- Leave Unassigned --'"></option>
|
|
<template x-for="session in whatsappSessions" :key="session.id">
|
|
<option :value="session.id" x-text="session.name + (session.phone ? ' (' + session.phone + ')' : '')"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
|
|
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showAddStaffModal = 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="lang === 'ar' ? 'إضافة الموظف' : 'Add Agent'"></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">×</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">×</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">×</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
|
|
superAdminStats: null,
|
|
superAdminCompanies: [],
|
|
superAdminPending: [],
|
|
superAdminPlans: [],
|
|
activeDashboardTab: 'whatsapp',
|
|
whatsappSession: null,
|
|
whatsappSessions: [],
|
|
whatsappMaxSessions: 1,
|
|
newSessionName: '',
|
|
staff: [],
|
|
showAddStaffModal: false,
|
|
staffForm: {
|
|
name: '',
|
|
email: '',
|
|
password: '',
|
|
whatsapp_session_id: ''
|
|
},
|
|
// Billing State
|
|
availablePlans: [],
|
|
userPlanId: null,
|
|
showCheckoutModal: false,
|
|
selectedCheckoutPlan: null,
|
|
checkoutPaymentMethod: 'paymob',
|
|
checkoutReceipt: '',
|
|
checkoutError: '',
|
|
checkoutSuccess: '',
|
|
|
|
woocommerceStatus: null,
|
|
woocommerceLoading: false,
|
|
wooForm: {
|
|
store_url: '',
|
|
consumer_key: '',
|
|
consumer_secret: '',
|
|
webhook_secret: ''
|
|
},
|
|
otpPhone: '',
|
|
otpType: 'image',
|
|
otpSessionId: '',
|
|
otpStatusMsg: '',
|
|
otpErrorCode: '',
|
|
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.whatsappSessions = [];
|
|
this.staff = [];
|
|
this.woocommerceStatus = null;
|
|
this.contacts = [];
|
|
this.templates = [];
|
|
this.campaigns = [];
|
|
this.endpoints = [];
|
|
this.sallaStatus = null;
|
|
this.dashboardSuccess = '';
|
|
this.dashboardError = '';
|
|
},
|
|
|
|
initializeDashboard() {
|
|
this.fetchWhatsappSessions();
|
|
this.fetchWhatsappStatus();
|
|
this.fetchSallaStatus();
|
|
this.fetchWooCommerceStatus();
|
|
// 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);
|
|
}
|
|
},
|
|
|
|
// Billing & Checkout Methods
|
|
async fetchPlans() {
|
|
try {
|
|
const response = await fetch('/api/plans', {
|
|
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
this.availablePlans = data.data;
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch plans', err);
|
|
}
|
|
},
|
|
|
|
openCheckoutModal(plan) {
|
|
this.selectedCheckoutPlan = plan;
|
|
this.checkoutPaymentMethod = 'paymob';
|
|
this.checkoutReceipt = '';
|
|
this.checkoutError = '';
|
|
this.checkoutSuccess = '';
|
|
this.showCheckoutModal = true;
|
|
},
|
|
|
|
async submitCheckout() {
|
|
if (this.checkoutPaymentMethod !== 'paymob' && !this.checkoutReceipt) {
|
|
this.checkoutError = this.lang === 'ar' ? 'الرجاء إدخال رقم الحوالة' : 'Please enter receipt reference';
|
|
return;
|
|
}
|
|
|
|
this.actionLoading = true;
|
|
this.checkoutError = '';
|
|
try {
|
|
const response = await fetch('/api/billing/upgrade', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
},
|
|
body: JSON.stringify({
|
|
plan_id: this.selectedCheckoutPlan.id,
|
|
payment_method: this.checkoutPaymentMethod,
|
|
receipt_reference: this.checkoutReceipt
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
if (this.checkoutPaymentMethod === 'paymob' && data.checkout_url) {
|
|
window.location.href = data.checkout_url;
|
|
} else {
|
|
this.checkoutSuccess = data.message;
|
|
setTimeout(() => {
|
|
this.showCheckoutModal = false;
|
|
}, 3000);
|
|
}
|
|
} else {
|
|
this.checkoutError = data.error || 'Failed to submit payment request';
|
|
}
|
|
} catch (err) {
|
|
this.checkoutError = 'Network error while submitting payment';
|
|
} finally {
|
|
this.actionLoading = false;
|
|
}
|
|
},
|
|
|
|
async fetchWhatsappStatus() {
|
|
if (!this.token) return;
|
|
const queryParam = this.whatsappSession ? `?session_id=${this.whatsappSession.id}` : '';
|
|
try {
|
|
const response = await fetch(`/api/whatsapp/status${queryParam}`, {
|
|
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());
|
|
}
|
|
|
|
if (this.whatsappSessions && this.whatsappSessions.length > 0 && this.whatsappSession) {
|
|
const idx = this.whatsappSessions.findIndex(s => s.id === this.whatsappSession.id);
|
|
if (idx !== -1) {
|
|
this.whatsappSessions[idx] = this.whatsappSession;
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to retrieve session status:', err);
|
|
}
|
|
},
|
|
// Super Admin methods
|
|
async fetchSuperAdminStats() {
|
|
if (!this.user?.is_super_admin) return;
|
|
this.actionLoading = true;
|
|
try {
|
|
const response = await fetch('/api/admin/stats', {
|
|
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
this.superAdminStats = data.data.stats;
|
|
this.superAdminCompanies = data.data.companies;
|
|
this.superAdminPending = data.data.pending_approvals;
|
|
this.superAdminPlans = data.data.plans;
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
this.actionLoading = false;
|
|
}
|
|
},
|
|
async changeCompanyPlan(companyId, planId) {
|
|
if (!planId) return;
|
|
if (!confirm(this.lang === 'ar' ? 'هل أنت متأكد من تغيير الباقة لهذه الشركة؟' : 'Are you sure you want to change the subscription plan for this company?')) return;
|
|
|
|
this.actionLoading = true;
|
|
try {
|
|
const response = await fetch('/api/admin/companies/subscribe', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
},
|
|
body: JSON.stringify({
|
|
company_id: companyId,
|
|
plan_id: planId,
|
|
duration_days: 30
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
alert(this.lang === 'ar' ? 'تم تحديث الباقة بنجاح' : 'Plan updated successfully');
|
|
await this.fetchSuperAdminStats();
|
|
} else {
|
|
alert(data.error || 'Failed to update plan');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
this.actionLoading = false;
|
|
}
|
|
},
|
|
async fetchWhatsappSessions() {
|
|
if (!this.token) return;
|
|
try {
|
|
const response = await fetch('/api/whatsapp/sessions', {
|
|
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
this.whatsappSessions = data.data || [];
|
|
this.whatsappMaxSessions = data.max_sessions || 1;
|
|
|
|
if (this.whatsappSessions.length > 0 && (!this.whatsappSession || !this.whatsappSessions.find(s => s.id === this.whatsappSession.id))) {
|
|
this.whatsappSession = this.whatsappSessions[0];
|
|
this.fetchChatbotSettings();
|
|
}
|
|
if (this.whatsappSessions.length === 0) {
|
|
this.whatsappSession = null;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to retrieve sessions list:', err);
|
|
}
|
|
},
|
|
|
|
async createWhatsappSession() {
|
|
this.actionLoading = true;
|
|
try {
|
|
const response = await fetch('/api/whatsapp/sessions', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
},
|
|
body: JSON.stringify({ name: this.newSessionName })
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
this.newSessionName = '';
|
|
await this.fetchWhatsappSessions();
|
|
} else {
|
|
alert(data.message || 'Failed to create WhatsApp session');
|
|
}
|
|
} catch (err) {
|
|
alert('Error communicating with backend Gateway API.');
|
|
} finally {
|
|
this.actionLoading = false;
|
|
}
|
|
},
|
|
|
|
async deleteWhatsappSession(sessionId) {
|
|
if (!confirm('Are you sure you want to delete this WhatsApp session? This will remove all associated connection settings.')) return;
|
|
this.actionLoading = true;
|
|
try {
|
|
const response = await fetch('/api/whatsapp/sessions', {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
},
|
|
body: JSON.stringify({ session_id: sessionId })
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
if (this.whatsappSession && this.whatsappSession.id === sessionId) {
|
|
this.whatsappSession = null;
|
|
}
|
|
await this.fetchWhatsappSessions();
|
|
} else {
|
|
alert(data.message || 'Failed to delete session');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error deleting session:', err);
|
|
} finally {
|
|
this.actionLoading = false;
|
|
}
|
|
},
|
|
|
|
async connectWhatsapp(sessionId) {
|
|
this.actionLoading = true;
|
|
try {
|
|
const response = await fetch('/api/whatsapp/qr', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ session_id: sessionId })
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
const found = this.whatsappSessions.find(s => s.id === sessionId);
|
|
if (found) {
|
|
this.whatsappSession = found;
|
|
this.whatsappSession.status = 'connecting';
|
|
}
|
|
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(sessionId) {
|
|
if (!confirm('Are you sure you want to disconnect this 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'
|
|
},
|
|
body: JSON.stringify({ session_id: sessionId })
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
await this.fetchWhatsappSessions();
|
|
if (this.whatsappSession && this.whatsappSession.id === sessionId) {
|
|
await this.fetchWhatsappStatus();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
this.actionLoading = false;
|
|
}
|
|
},
|
|
|
|
// Customer Service Staff methods
|
|
async fetchStaff() {
|
|
this.staffLoading = true;
|
|
try {
|
|
const response = await fetch('/api/staff', {
|
|
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
this.staff = data.data || [];
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching staff list:', err);
|
|
} finally {
|
|
this.staffLoading = false;
|
|
}
|
|
},
|
|
|
|
async submitAddStaff() {
|
|
this.actionLoading = true;
|
|
try {
|
|
const response = await fetch('/api/staff', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
},
|
|
body: JSON.stringify(this.staffForm)
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
this.showAddStaffModal = false;
|
|
this.staffForm = { name: '', email: '', password: '', whatsapp_session_id: '' };
|
|
await this.fetchStaff();
|
|
} else {
|
|
const errs = data.errors || {};
|
|
const firstErr = Object.values(errs)[0]?.[0] || data.error || 'Failed to create agent';
|
|
alert(firstErr);
|
|
}
|
|
} catch (err) {
|
|
alert('Network error while adding agent');
|
|
} finally {
|
|
this.actionLoading = false;
|
|
}
|
|
},
|
|
|
|
async deleteStaff(agentId) {
|
|
if (!confirm('Are you sure you want to remove this customer service agent?')) return;
|
|
this.actionLoading = true;
|
|
try {
|
|
const response = await fetch('/api/staff', {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
},
|
|
body: JSON.stringify({ agent_id: agentId })
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
await this.fetchStaff();
|
|
} else {
|
|
alert(data.error || 'Failed to delete agent');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
this.actionLoading = false;
|
|
}
|
|
},
|
|
|
|
async assignSessionToStaff(agentId, sessionId) {
|
|
try {
|
|
const response = await fetch('/api/staff/assign', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
},
|
|
body: JSON.stringify({
|
|
agent_id: agentId,
|
|
whatsapp_session_id: sessionId ? parseInt(sessionId) : null
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
await this.fetchStaff();
|
|
} else {
|
|
alert(data.error || 'Failed to assign session');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error assigning session:', err);
|
|
}
|
|
},
|
|
|
|
// WooCommerce Integration methods
|
|
async fetchWooCommerceStatus() {
|
|
try {
|
|
const response = await fetch('/api/integrations/woocommerce/status', {
|
|
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
this.woocommerceStatus = data;
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching WooCommerce status:', err);
|
|
}
|
|
},
|
|
|
|
async connectWooCommerce() {
|
|
this.woocommerceLoading = true;
|
|
try {
|
|
const response = await fetch('/api/integrations/woocommerce/connect', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
},
|
|
body: JSON.stringify(this.wooForm)
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
await this.fetchWooCommerceStatus();
|
|
} else {
|
|
alert(data.message || 'Failed to connect WooCommerce store');
|
|
}
|
|
} catch (err) {
|
|
alert('Network error while connecting WooCommerce');
|
|
} finally {
|
|
this.woocommerceLoading = false;
|
|
}
|
|
},
|
|
|
|
async disconnectWooCommerce() {
|
|
if (!confirm('Are you sure you want to disconnect WooCommerce integration? Webhooks will no longer notify customers.')) return;
|
|
this.woocommerceLoading = true;
|
|
try {
|
|
const response = await fetch('/api/integrations/woocommerce/disconnect', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
}
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
this.wooForm = { store_url: '', consumer_key: '', consumer_secret: '', webhook_secret: '' };
|
|
await this.fetchWooCommerceStatus();
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
this.woocommerceLoading = false;
|
|
}
|
|
},
|
|
|
|
// OTP Testing Tool methods
|
|
async sendOtpTest() {
|
|
if (!this.otpSessionId) {
|
|
alert('Please select a WhatsApp line to send from.');
|
|
return;
|
|
}
|
|
this.actionLoading = true;
|
|
this.otpStatusMsg = '';
|
|
this.otpErrorCode = '';
|
|
try {
|
|
const response = await fetch('/api/otp/send', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.token}`
|
|
},
|
|
body: JSON.stringify({
|
|
phone: this.otpPhone,
|
|
type: this.otpType,
|
|
session_id: parseInt(this.otpSessionId)
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok && data.status === 'success') {
|
|
this.otpStatusMsg = this.lang === 'ar'
|
|
? `✓ تم إرسال رمز التحقق بنجاح! الرمز المرسل هو: ${data.code}`
|
|
: `✓ OTP delivered successfully! Code sent: ${data.code}`;
|
|
} else {
|
|
this.otpErrorCode = 'error';
|
|
this.otpStatusMsg = data.error || 'Failed to deliver OTP';
|
|
}
|
|
} catch (err) {
|
|
this.otpErrorCode = 'error';
|
|
this.otpStatusMsg = 'Network error while delivering OTP';
|
|
} 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 === 'intaleq') {
|
|
this.chatbotSettings.ai_prompt = `أنت روبوت خدمة العملاء الخاص بشركة "انطلق" (Intaleq). مهمتك هي مساعدة المستخدمين والإجابة على استفساراتهم حول خدماتنا بلهجة سورية ودودة ومهنية للغاية.
|
|
|
|
التزم بالقواعد والتفاصيل التالية بدقة عند الرد:
|
|
1. اسم الشركة: انطلق.
|
|
2. ساعات العمل: يومياً من الساعة 11 صباحاً حتى 5 مساءً.
|
|
3. طرق الدفع المتاحة: شام كاش، سيريتل، إم تي إن، وبطاقات البنك.
|
|
4. طريقة التسجيل: يمكن للمستخدمين تحميل التطبيق من متجر جوجل بلاي، آبل ستور، أو عبر رابط مباشر. عملية التسجيل سهلة وسريعة.
|
|
5. مميزات التطبيق:
|
|
- عمولة التطبيق هي 10% فقط (وهي الأقل في السوق).
|
|
- يوجد ذكاء اصطناعي متطور لتحليل المشاكل وحلها.
|
|
- ضمان حقوق السائق والركاب بشكل كامل.
|
|
- خدمة الطلب من أي مكان وبكل سهولة.
|
|
6. تحدث دائماً باللهجة السورية اللطيفة والترحيبية والودية عند التحدث باللغة العربية (مثال: "ياهلا بك"، "كيف بقدر أساعدك اليوم؟").
|
|
7. كوني مختصرة ومباشرة ومفيدة في إجاباتكِ وتجنبي الإطالة غير الضرورية.`;
|
|
} else 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>
|