Deploy: 2026-05-23 01:13:51

This commit is contained in:
Hamza-Ayed
2026-05-23 01:13:51 +03:00
parent 681ef6afab
commit 57859ebd20
11 changed files with 2426 additions and 108 deletions

View File

@@ -774,9 +774,12 @@
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'chatbot' }" @click="activeDashboardTab = 'chatbot'; fetchChatbotSettings()" id="nav-chatbot-btn">
<span>🤖</span> <span x-text="lang === 'ar' ? 'روبوت الذكاء الاصطناعي' : 'AI Chatbot Settings'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'integrations' }" @click="activeDashboardTab = 'integrations'; fetchEndpoints(); fetchSallaStatus()" id="nav-integrations-btn">
<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 -->
@@ -797,107 +800,159 @@
<!-- Panel: WhatsApp Connection -->
<div class="panel" x-show="activeDashboardTab === 'whatsapp'" id="panel-whatsapp">
<h2 style="font-size: 1.4rem; margin-bottom: 1.5rem;">WhatsApp Integration</h2>
<div class="grid-two">
<!-- Connection Control Card -->
<div class="status-box">
<div class="status-badge" :class="{
'badge-disconnected': !whatsappSession || whatsappSession.status === 'disconnected',
'badge-connecting': whatsappSession && whatsappSession.status === 'connecting',
'badge-waiting_qr': whatsappSession && whatsappSession.status === 'waiting_qr',
'badge-connected': whatsappSession && whatsappSession.status === 'connected'
}">
<span x-text="whatsappSession ? whatsappSession.status : 'disconnected'"></span>
</div>
<h3 style="font-size: 1.25rem; margin-bottom: 0.5rem;">
<template x-if="whatsappSession && whatsappSession.status === 'connected'">
<span>WhatsApp Connected</span>
</template>
<template x-if="!whatsappSession || whatsappSession.status !== 'connected'">
<span>Session Inactive</span>
</template>
</h3>
<p class="text-muted" style="font-size: 0.9rem; margin-bottom: 1.5rem; max-width: 250px;">
<template x-if="whatsappSession && whatsappSession.phone">
<span>Active number: <strong x-text="whatsappSession.phone" style="color: var(--text-primary);"></strong></span>
</template>
<template x-if="!whatsappSession || !whatsappSession.phone">
<span>Start a connection session to link your phone.</span>
</template>
</p>
<!-- Actions -->
<template x-if="!whatsappSession || whatsappSession.status === 'disconnected'">
<button @click="connectWhatsapp()" class="btn btn-primary" :disabled="actionLoading" id="btn-request-qr">
<span x-show="!actionLoading">Generate QR Code</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</template>
<template x-if="whatsappSession && whatsappSession.status !== 'disconnected'">
<button @click="disconnectWhatsapp()" class="btn btn-danger" :disabled="actionLoading" id="btn-disconnect-session">
<span x-show="!actionLoading">Disconnect Session</span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</template>
<div 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;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<input type="text" x-model="newSessionName" :placeholder="lang === 'ar' ? 'اسم الرقم (مثال: الدعم)' : 'Session Name (e.g. Sales)'" class="form-input" style="max-width: 200px; padding: 0.5rem 0.8rem; font-size: 0.85rem;" 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">
<span x-text="lang === 'ar' ? '+ إضافة خط جديد' : '+ Add New Line'"></span>
</button>
</div>
</div>
<!-- QR Display Card -->
<div class="flex-center flex-column panel" style="background: rgba(10, 11, 20, 0.2); border-color: rgba(255, 255, 255, 0.03);" id="qr-display-container">
<template x-if="whatsappSession && whatsappSession.status === 'connecting'">
<div class="text-center">
<div class="spinner spinner-large" style="margin-bottom: 1rem;"></div>
<p class="font-semibold">Establishing Connection...</p>
<p class="text-muted" style="font-size: 0.85rem; margin-top: 0.25rem;">Checking gateway processes and requesting channel</p>
</div>
</template>
<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>
<template x-if="whatsappSession && whatsappSession.status === 'waiting_qr'">
<div class="text-center">
<p class="font-semibold">Scan QR Code</p>
<p class="text-muted" style="font-size: 0.85rem; margin-top: 0.25rem;">Scan using Link Devices inside WhatsApp</p>
<div class="qr-wrapper">
<div id="qrcode-canvas" x-init="$nextTick(() => renderQr())"></div>
<!-- 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>
<!-- Diagnostics -->
<div style="font-size: 0.75rem; margin: 0.5rem 0; display: flex; flex-direction: column; gap: 0.25rem;">
<template x-if="!whatsappSession.qr_code">
<span style="color: var(--danger-accent);">⚠️ Decryption issue: QR code string is empty.</span>
<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="whatsappSession.qr_code">
<span style="color: var(--success-accent);">✓ Encrypted QR data retrieved successfully.</span>
<template x-if="!session.phone">
<span x-text="lang === 'ar' ? 'الخط غير مرتبط برقم هاتف بعد.' : 'No phone linked yet. Scan QR code.'"></span>
</template>
<template x-if="typeof window.QRCode === 'undefined'">
<span style="color: var(--danger-accent);">⚠️ QRCode library failed to load (Integrity/CSP issue).</span>
</template>
</div>
<div style="font-size: 0.8rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;" class="text-muted">
<div class="spinner" style="width: 14px; height: 14px; border-width: 2px;"></div>
<span>Waiting for connection handshake...</span>
</div>
</div>
</template>
<template x-if="whatsappSession && whatsappSession.status === 'connected'">
<div class="text-center" style="padding: 2rem 0;">
<div style="font-size: 3rem; color: var(--success-accent); margin-bottom: 0.5rem; text-shadow: 0 0 20px var(--success-glow);"></div>
<p class="font-semibold" style="font-size: 1.15rem; color: var(--success-accent);">Gateway fully connected</p>
<p class="text-muted" style="font-size: 0.85rem; margin-top: 0.25rem; max-width: 250px; margin-left: auto; margin-right: auto;">
You can now create templates and launch marketing broadcast campaigns.
</p>
</div>
</template>
<template x-if="!whatsappSession || whatsappSession.status === 'disconnected'">
<div class="text-center text-muted" style="padding: 3rem 0;">
<span style="font-size: 2.5rem; display: block; margin-bottom: 0.5rem;">🔌</span>
<p>No active WhatsApp link</p>
<div 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">&times;</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="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>
@@ -1171,6 +1226,84 @@
</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>
@@ -1205,6 +1338,59 @@
</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>
@@ -1245,6 +1431,48 @@
</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">&times;</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">
@@ -1422,6 +1650,29 @@
// Dashboard States
activeDashboardTab: 'whatsapp',
whatsappSession: null,
whatsappSessions: [],
newSessionName: '',
staff: [],
showAddStaffModal: false,
staffForm: {
name: '',
email: '',
password: '',
whatsapp_session_id: ''
},
woocommerceStatus: null,
woocommerceLoading: false,
wooForm: {
store_url: '',
consumer_key: '',
consumer_secret: '',
webhook_secret: ''
},
otpPhone: '',
otpType: 'text',
otpSessionId: '',
otpStatusMsg: '',
otpErrorCode: '',
contacts: [],
selectedContactIds: [],
bulkGroupId: '',
@@ -1582,6 +1833,9 @@
this.user = null;
this.isLoggedIn = false;
this.whatsappSession = null;
this.whatsappSessions = [];
this.staff = [];
this.woocommerceStatus = null;
this.contacts = [];
this.templates = [];
this.campaigns = [];
@@ -1592,8 +1846,10 @@
},
initializeDashboard() {
this.fetchWhatsappSessions();
this.fetchWhatsappStatus();
this.fetchSallaStatus();
this.fetchWooCommerceStatus();
// Set up persistent background status check
this.startPolling();
@@ -1615,8 +1871,9 @@
async fetchWhatsappStatus() {
if (!this.token) return;
const queryParam = this.whatsappSession ? `?session_id=${this.whatsappSession.id}` : '';
try {
const response = await fetch('/api/whatsapp/status', {
const response = await fetch(`/api/whatsapp/status${queryParam}`, {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
@@ -1625,13 +1882,88 @@
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);
}
},
async connectWhatsapp() {
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 || [];
}
} 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', {
@@ -1639,10 +1971,16 @@
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');
@@ -1654,8 +1992,8 @@
}
},
async disconnectWhatsapp() {
if (!confirm('Are you sure you want to disconnect your WhatsApp link?')) return;
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', {
@@ -1663,14 +2001,214 @@
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') {
await this.fetchWhatsappStatus();
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;
}