Deploy: 2026-05-23 01:13:51
This commit is contained in:
49
backend/add_whatsapp_session_id_to_users.php
Normal file
49
backend/add_whatsapp_session_id_to_users.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
http_response_code(403);
|
||||
exit('Access denied.');
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/app/bootstrap.php';
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
try {
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
echo "=== Running Database Migrations: Adding whatsapp_session_id ===\n";
|
||||
|
||||
// Check if the column already exists
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'users'
|
||||
AND COLUMN_NAME = 'whatsapp_session_id'
|
||||
");
|
||||
$stmt->execute();
|
||||
$columnExists = $stmt->fetch();
|
||||
|
||||
if (!$columnExists) {
|
||||
echo "Adding column 'whatsapp_session_id' to 'users' table...\n";
|
||||
|
||||
// Add column
|
||||
$pdo->exec("ALTER TABLE `users` ADD COLUMN `whatsapp_session_id` INT NULL DEFAULT NULL");
|
||||
|
||||
// Add foreign key constraint
|
||||
$pdo->exec("
|
||||
ALTER TABLE `users`
|
||||
ADD CONSTRAINT `fk_users_whatsapp_session`
|
||||
FOREIGN KEY (`whatsapp_session_id`)
|
||||
REFERENCES `whatsapp_sessions` (`id`)
|
||||
ON DELETE SET NULL
|
||||
");
|
||||
|
||||
echo "✅ Column 'whatsapp_session_id' and foreign key created successfully!\n";
|
||||
} else {
|
||||
echo "ℹ️ Column 'whatsapp_session_id' already exists in 'users' table. Skipping.\n";
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
echo "❌ Migration failed: " . $e->getMessage() . "\n";
|
||||
}
|
||||
141
backend/app/Controllers/OTPController.php
Normal file
141
backend/app/Controllers/OTPController.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Request;
|
||||
use App\Core\Response;
|
||||
use App\Core\Flows\ConversationFlowEngine;
|
||||
use App\Services\TTSService;
|
||||
use App\Models\WhatsAppSession;
|
||||
use App\Models\CompanySubscription;
|
||||
use App\Models\CompanySubscriptionUsage;
|
||||
|
||||
class OTPController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Send OTP verification code via WhatsApp (Text or Voice Note)
|
||||
* POST /api/otp/send
|
||||
*/
|
||||
public function send(Request $request, Response $response): void
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
$body = $request->getBody();
|
||||
|
||||
$phone = $body['phone'] ?? '';
|
||||
$type = $body['type'] ?? 'text'; // 'text' or 'voice'
|
||||
$sessionId = $body['session_id'] ?? null;
|
||||
$customCode = $body['code'] ?? null;
|
||||
|
||||
if (empty($phone)) {
|
||||
$response->status(400)->json(['error' => 'Missing required parameter: phone']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean phone number (remove non-digits except +)
|
||||
$phone = preg_replace('/[^\d+]/', '', $phone);
|
||||
|
||||
// 1. Resolve WhatsApp Session
|
||||
$session = null;
|
||||
if ($sessionId) {
|
||||
$session = WhatsAppSession::findSecure((int)$sessionId);
|
||||
if (!$session || (int)$session['company_id'] !== (int)$companyId) {
|
||||
$response->status(404)->json(['error' => 'WhatsApp session not found']);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Grab the first connected session of the company
|
||||
$sessions = WhatsAppSession::findAllByCompany($companyId);
|
||||
foreach ($sessions as $s) {
|
||||
if ($s['status'] === 'connected') {
|
||||
$session = $s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$session && !empty($sessions)) {
|
||||
$session = $sessions[0]; // fallback to first session if none is connected
|
||||
}
|
||||
}
|
||||
|
||||
if (!$session) {
|
||||
$response->status(400)->json(['error' => 'No active WhatsApp sessions configured for this company.']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($session['status'] !== 'connected') {
|
||||
$response->status(400)->json(['error' => 'WhatsApp session is not connected. Connect the session first.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check SaaS subscription quotas
|
||||
if ($companyId !== 1) {
|
||||
$activeSub = CompanySubscription::findActiveByCompany($companyId);
|
||||
if (!$activeSub) {
|
||||
$response->status(402)->json(['error' => 'Active subscription plan required.']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CompanySubscriptionUsage::hasRemainingLimit($companyId, 'request')) {
|
||||
$response->status(403)->json(['error' => 'Monthly request quota exceeded. Please upgrade your plan.']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'voice') {
|
||||
if (!CompanySubscriptionUsage::hasRemainingLimit($companyId, 'voice')) {
|
||||
$response->status(403)->json(['error' => 'Voice request quota exceeded. Please upgrade your plan.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Starter plan doesn't support Voice Notes
|
||||
$features = json_decode($activeSub['features'] ?: '{}', true);
|
||||
if (isset($features['voice']) && !$features['voice']) {
|
||||
$response->status(403)->json(['error' => 'Voice OTP is not supported in your current subscription plan.']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Generate verification code
|
||||
$code = $customCode ? trim($customCode) : (string)rand(1000, 9999);
|
||||
|
||||
// 4. Send Message
|
||||
try {
|
||||
if ($type === 'voice') {
|
||||
// Spacing the digits to force slow Arabic pronunciation: e.g. "1 2 3 4"
|
||||
$spacedCode = implode(' ', str_split($code));
|
||||
$textToRead = "رمز التحقق الخاص بك هو: {$spacedCode}. أكرر، رمز التحقق هو: {$spacedCode}.";
|
||||
|
||||
$audioBase64 = TTSService::textToSpeechArabic($textToRead);
|
||||
if (!$audioBase64) {
|
||||
$response->status(500)->json(['error' => 'Failed to generate voice OTP audio.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send voice note
|
||||
ConversationFlowEngine::sendReply($session, $phone, '', null, $audioBase64, 'audio/mp3');
|
||||
} else {
|
||||
// Send text
|
||||
$textMsg = "رمز التحقق الخاص بك لمتجر نابه هو: *{$code}* \n الرجاء عدم مشاركته مع أي شخص.";
|
||||
ConversationFlowEngine::sendReply($session, $phone, $textMsg);
|
||||
}
|
||||
|
||||
// Increment usage stats
|
||||
if ($companyId !== 1) {
|
||||
CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
||||
if ($type === 'voice') {
|
||||
CompanySubscriptionUsage::incrementUsage($companyId, 'voice');
|
||||
}
|
||||
}
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'message' => 'OTP sent successfully',
|
||||
'code' => $code,
|
||||
'type' => $type
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log("[OTP Controller Error] " . $e->getMessage());
|
||||
$response->status(500)->json(['error' => 'Failed to send OTP message: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
184
backend/app/Controllers/StaffController.php
Normal file
184
backend/app/Controllers/StaffController.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Request;
|
||||
use App\Core\Response;
|
||||
use App\Core\Security;
|
||||
use App\Models\User;
|
||||
use App\Models\WhatsAppSession;
|
||||
use App\Core\Database;
|
||||
|
||||
class StaffController extends BaseController
|
||||
{
|
||||
/**
|
||||
* List all staff agents for the merchant company
|
||||
* GET /api/staff
|
||||
*/
|
||||
public function index(Request $request, Response $response): void
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
|
||||
// Fetch users belonging to this company who are 'staff'
|
||||
$staff = Database::select(
|
||||
"SELECT u.id, u.name, u.email, u.role, u.status, u.whatsapp_session_id, w.name as session_name, w.phone as session_phone
|
||||
FROM users u
|
||||
LEFT JOIN whatsapp_sessions w ON u.whatsapp_session_id = w.id
|
||||
WHERE u.company_id = ? AND u.role = 'staff'
|
||||
ORDER BY u.id DESC",
|
||||
[$companyId]
|
||||
);
|
||||
|
||||
foreach ($staff as &$member) {
|
||||
$member['email'] = Security::decrypt($member['email']);
|
||||
if (!empty($member['session_phone'])) {
|
||||
$member['session_phone'] = Security::decrypt($member['session_phone']);
|
||||
}
|
||||
}
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'data' => $staff
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new customer service agent (staff)
|
||||
* POST /api/staff
|
||||
*/
|
||||
public function store(Request $request, Response $response): void
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
$errors = $this->validate($request, [
|
||||
'name' => 'required|min:3',
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:6'
|
||||
]);
|
||||
|
||||
if (!empty($errors)) {
|
||||
$response->json(['errors' => $errors], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$body = $request->getBody();
|
||||
$email = strtolower(trim($body['email']));
|
||||
|
||||
// Check if user already exists
|
||||
$existing = User::findByEmail($email);
|
||||
if ($existing) {
|
||||
$response->json(['errors' => ['email' => ['This email is already registered.']]], 409);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate session if assigned
|
||||
$whatsappSessionId = isset($body['whatsapp_session_id']) && $body['whatsapp_session_id'] !== '' ? (int)$body['whatsapp_session_id'] : null;
|
||||
if ($whatsappSessionId) {
|
||||
$session = WhatsAppSession::findSecure($whatsappSessionId);
|
||||
if (!$session || (int)$session['company_id'] !== (int)$companyId) {
|
||||
$response->status(400)->json(['error' => 'Invalid WhatsApp session assigned']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$userId = User::createSecure([
|
||||
'company_id' => $companyId,
|
||||
'name' => trim($body['name']),
|
||||
'email' => $email,
|
||||
'password' => $body['password'],
|
||||
'role' => 'staff',
|
||||
'status' => 'active',
|
||||
'whatsapp_session_id' => $whatsappSessionId
|
||||
]);
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Agent created successfully',
|
||||
'data' => [
|
||||
'id' => $userId,
|
||||
'name' => trim($body['name']),
|
||||
'email' => $email,
|
||||
'role' => 'staff',
|
||||
'whatsapp_session_id' => $whatsappSessionId
|
||||
]
|
||||
], 201);
|
||||
} catch (\Exception $e) {
|
||||
error_log("[Staff Controller Error] " . $e->getMessage());
|
||||
$response->status(500)->json(['error' => 'Failed to create agent: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an agent
|
||||
* DELETE /api/staff
|
||||
*/
|
||||
public function delete(Request $request, Response $response): void
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
$body = $request->getBody();
|
||||
$agentId = $body['agent_id'] ?? null;
|
||||
|
||||
if (!$agentId) {
|
||||
$response->status(400)->json(['error' => 'Missing agent_id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = User::find($agentId);
|
||||
if (!$user || (int)$user['company_id'] !== (int)$companyId || $user['role'] !== 'staff') {
|
||||
$response->status(404)->json(['error' => 'Agent not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
User::delete((int)$agentId);
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Agent deleted successfully'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a specific WhatsApp session to an agent
|
||||
* PUT /api/staff/assign
|
||||
*/
|
||||
public function assignSession(Request $request, Response $response): void
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
$body = $request->getBody();
|
||||
|
||||
$agentId = $body['agent_id'] ?? null;
|
||||
$whatsappSessionId = isset($body['whatsapp_session_id']) && $body['whatsapp_session_id'] !== '' ? (int)$body['whatsapp_session_id'] : null;
|
||||
|
||||
if (!$agentId) {
|
||||
$response->status(400)->json(['error' => 'Missing agent_id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = User::find($agentId);
|
||||
if (!$user || (int)$user['company_id'] !== (int)$companyId || $user['role'] !== 'staff') {
|
||||
$response->status(404)->json(['error' => 'Agent not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($whatsappSessionId) {
|
||||
$session = WhatsAppSession::findSecure($whatsappSessionId);
|
||||
if (!$session || (int)$session['company_id'] !== (int)$companyId) {
|
||||
$response->status(400)->json(['error' => 'Invalid WhatsApp session']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
User::update((int)$agentId, [
|
||||
'whatsapp_session_id' => $whatsappSessionId
|
||||
]);
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'message' => 'WhatsApp session successfully assigned to agent'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$response->status(500)->json(['error' => 'Failed to assign session: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
backend/app/Controllers/SuperAdminController.php
Normal file
141
backend/app/Controllers/SuperAdminController.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Request;
|
||||
use App\Core\Response;
|
||||
use App\Core\Database;
|
||||
use App\Models\CompanySubscription;
|
||||
|
||||
class SuperAdminController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Helper to verify if the requester is the Super Admin (Company ID 1 and role Admin)
|
||||
*/
|
||||
private function verifySuperAdmin(Request $request, Response $response): bool
|
||||
{
|
||||
if ((int)$request->company_id !== 1 || $request->role !== 'admin') {
|
||||
$response->status(403)->json(['error' => 'Forbidden: Super Admin privileges required.']);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform statistics and companies list
|
||||
* GET /api/admin/stats
|
||||
*/
|
||||
public function getStats(Request $request, Response $response): void
|
||||
{
|
||||
if (!$this->verifySuperAdmin($request, $response)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Overall stats
|
||||
$companiesCount = Database::selectOne("SELECT COUNT(*) as count FROM companies")['count'] ?? 0;
|
||||
$sessionsCount = Database::selectOne("SELECT COUNT(*) as count FROM whatsapp_sessions")['count'] ?? 0;
|
||||
$connectedSessions = Database::selectOne("SELECT COUNT(*) as count FROM whatsapp_sessions WHERE status = 'connected'")['count'] ?? 0;
|
||||
|
||||
// Detailed list of all companies and their current subscriptions
|
||||
$companies = Database::select("
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.status,
|
||||
cs.plan_id,
|
||||
sp.name as plan_name,
|
||||
cs.status as subscription_status,
|
||||
cs.starts_at as subscription_starts,
|
||||
cs.ends_at as subscription_ends,
|
||||
(SELECT COUNT(*) FROM whatsapp_sessions WHERE company_id = c.id) as sessions_count,
|
||||
(SELECT COUNT(*) FROM whatsapp_sessions WHERE company_id = c.id AND status = 'connected') as active_sessions,
|
||||
COALESCE(cu.request_count, 0) as request_usage,
|
||||
COALESCE(cu.voice_count, 0) as voice_usage,
|
||||
COALESCE(cu.ocr_count, 0) as ocr_usage
|
||||
FROM companies c
|
||||
LEFT JOIN company_subscriptions cs ON cs.company_id = c.id AND cs.status = 'active'
|
||||
LEFT JOIN subscription_plans sp ON cs.plan_id = sp.id
|
||||
LEFT JOIN company_subscription_usage cu ON cu.company_id = c.id
|
||||
AND cu.billing_start <= CURRENT_DATE()
|
||||
AND cu.billing_end >= CURRENT_DATE()
|
||||
ORDER BY c.id ASC
|
||||
");
|
||||
|
||||
// Fetch list of available subscription plans
|
||||
$plans = Database::select("SELECT id, name, price, max_sessions FROM subscription_plans ORDER BY price ASC");
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'stats' => [
|
||||
'total_companies' => (int)$companiesCount,
|
||||
'total_sessions' => (int)$sessionsCount,
|
||||
'connected_sessions' => (int)$connectedSessions
|
||||
],
|
||||
'companies' => $companies,
|
||||
'plans' => $plans
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log("[SuperAdminController Error] " . $e->getMessage());
|
||||
$response->status(500)->json(['error' => 'Failed to fetch platform stats: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe or upgrade a company to a plan
|
||||
* POST /api/admin/companies/subscribe
|
||||
*/
|
||||
public function subscribeCompany(Request $request, Response $response): void
|
||||
{
|
||||
if (!$this->verifySuperAdmin($request, $response)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = $request->getBody();
|
||||
$targetCompanyId = isset($body['company_id']) ? (int)$body['company_id'] : null;
|
||||
$planId = isset($body['plan_id']) ? (int)$body['plan_id'] : null;
|
||||
$durationDays = isset($body['duration_days']) ? (int)$body['duration_days'] : 30;
|
||||
|
||||
if (!$targetCompanyId || !$planId) {
|
||||
$response->status(400)->json(['error' => 'Missing company_id or plan_id']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify company exists
|
||||
$companyExists = Database::selectOne("SELECT id FROM companies WHERE id = ?", [$targetCompanyId]);
|
||||
if (!$companyExists) {
|
||||
$response->status(404)->json(['error' => 'Company not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify plan exists
|
||||
$planExists = Database::selectOne("SELECT id FROM subscription_plans WHERE id = ?", [$planId]);
|
||||
if (!$planExists) {
|
||||
$response->status(404)->json(['error' => 'Subscription plan not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Subscribe the company
|
||||
$subId = CompanySubscription::subscribeCompany($targetCompanyId, $planId, $durationDays, 'manual_admin', 'admin_' . $request->user_id);
|
||||
|
||||
// Clean active subscription cache for the company
|
||||
if (class_exists('App\Core\Cache')) {
|
||||
\App\Core\Cache::delete("company_subscription:{$targetCompanyId}");
|
||||
\App\Core\Cache::delete("company_subscription_{$targetCompanyId}");
|
||||
}
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Subscription updated successfully',
|
||||
'subscription_id' => $subId
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
error_log("[SuperAdminController Error] " . $e->getMessage());
|
||||
$response->status(500)->json(['error' => 'Failed to update subscription: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,17 @@ class WhatsAppController extends BaseController
|
||||
public function status(Request $request, Response $response)
|
||||
{
|
||||
$companyId = $request->company_id; // Added by AuthMiddleware
|
||||
$session = WhatsAppSession::findOrCreate($companyId);
|
||||
$sessionId = $request->get('session_id') ?? null;
|
||||
|
||||
if ($sessionId) {
|
||||
$session = WhatsAppSession::findSecure((int)$sessionId);
|
||||
if (!$session || (int)$session['company_id'] !== (int)$companyId) {
|
||||
$response->status(404)->json(['status' => 'error', 'message' => 'Session not found']);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$session = WhatsAppSession::findOrCreate($companyId);
|
||||
}
|
||||
|
||||
// Auto-heal logic: Check if the session is active in the Node.js gateway
|
||||
if ($session['status'] === 'connected' && !empty($session['session_key'])) {
|
||||
@@ -85,12 +95,121 @@ class WhatsAppController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a new connection/QR code from the Baileys service
|
||||
* Get all WhatsApp sessions for the company
|
||||
*/
|
||||
public function listSessions(Request $request, Response $response)
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
$sessions = WhatsAppSession::findAllByCompany($companyId);
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'data' => $sessions
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WhatsApp session
|
||||
*/
|
||||
public function createSession(Request $request, Response $response)
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
$body = $request->getBody();
|
||||
$name = !empty($body['name']) ? trim($body['name']) : 'WhatsApp Team';
|
||||
|
||||
// Fetch subscription limits
|
||||
$activeSub = \App\Models\CompanySubscription::findActiveByCompany($companyId);
|
||||
$maxSessions = 1;
|
||||
if ($companyId === 1) {
|
||||
$maxSessions = 10;
|
||||
} elseif ($activeSub) {
|
||||
$maxSessions = (int)$activeSub['max_sessions'];
|
||||
}
|
||||
|
||||
$sessions = WhatsAppSession::findAllByCompany($companyId);
|
||||
if (count($sessions) >= $maxSessions) {
|
||||
$response->status(400)->json([
|
||||
'status' => 'error',
|
||||
'message' => "You have reached the maximum number of WhatsApp sessions allowed by your plan ({$maxSessions})."
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionKey = 'cmp_' . $companyId . '_' . bin2hex(random_bytes(4));
|
||||
$id = WhatsAppSession::create([
|
||||
'company_id' => $companyId,
|
||||
'name' => $name,
|
||||
'session_key' => $sessionKey,
|
||||
'status' => 'disconnected'
|
||||
]);
|
||||
|
||||
$newSession = WhatsAppSession::findSecure((int)$id);
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'data' => $newSession
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing WhatsApp session
|
||||
*/
|
||||
public function deleteSession(Request $request, Response $response)
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
$body = $request->getBody();
|
||||
$sessionId = $body['session_id'] ?? null;
|
||||
|
||||
if (!$sessionId) {
|
||||
$response->status(400)->json(['status' => 'error', 'message' => 'Missing session_id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$session = WhatsAppSession::findSecure((int)$sessionId);
|
||||
if (!$session || (int)$session['company_id'] !== (int)$companyId) {
|
||||
$response->status(404)->json(['status' => 'error', 'message' => 'Session not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Call Baileys Node.js Service to delete session from memory
|
||||
$nodeUrl = 'http://127.0.0.1:3722/api/sessions/delete';
|
||||
$payload = json_encode(['session_key' => $session['session_key']]);
|
||||
|
||||
$ch = curl_init($nodeUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'X-Webhook-Secret: ' . getenv('WEBHOOK_SECRET')
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
WhatsAppSession::delete((int)$sessionId);
|
||||
|
||||
$response->json(['status' => 'success', 'message' => 'Session deleted successfully']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a new connection/QR code from the Baileys service for a specific session
|
||||
*/
|
||||
public function requestQr(Request $request, Response $response)
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
$session = WhatsAppSession::findOrCreate($companyId);
|
||||
$body = $request->getBody();
|
||||
$sessionId = $body['session_id'] ?? null;
|
||||
|
||||
if ($sessionId) {
|
||||
$session = WhatsAppSession::findSecure((int)$sessionId);
|
||||
if (!$session || (int)$session['company_id'] !== (int)$companyId) {
|
||||
$response->status(404)->json(['status' => 'error', 'message' => 'Session not found']);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$session = WhatsAppSession::findOrCreate($companyId);
|
||||
}
|
||||
|
||||
// Temporarily set to connecting
|
||||
WhatsAppSession::updateState($session['id'], ['status' => 'connecting']);
|
||||
@@ -133,12 +252,23 @@ class WhatsAppController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the current WhatsApp session
|
||||
* Disconnect the WhatsApp session
|
||||
*/
|
||||
public function disconnect(Request $request, Response $response)
|
||||
{
|
||||
$companyId = $request->company_id;
|
||||
$session = WhatsAppSession::findByCompany($companyId);
|
||||
$body = $request->getBody();
|
||||
$sessionId = $body['session_id'] ?? null;
|
||||
|
||||
if ($sessionId) {
|
||||
$session = WhatsAppSession::findSecure((int)$sessionId);
|
||||
if (!$session || (int)$session['company_id'] !== (int)$companyId) {
|
||||
$response->status(404)->json(['status' => 'error', 'message' => 'Session not found']);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$session = WhatsAppSession::findByCompany($companyId);
|
||||
}
|
||||
|
||||
if ($session && $session['status'] !== 'disconnected') {
|
||||
// Call Baileys Node.js Service to disconnect
|
||||
|
||||
@@ -32,6 +32,42 @@ class WhatsAppSession extends BaseModel
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find secure session by ID
|
||||
*/
|
||||
public static function findSecure(int $id)
|
||||
{
|
||||
$session = Database::selectOne(
|
||||
"SELECT * FROM " . static::$table . " WHERE id = ? LIMIT 1",
|
||||
[$id]
|
||||
);
|
||||
|
||||
if ($session) {
|
||||
$session['phone'] = $session['phone'] ? Security::decrypt($session['phone']) : null;
|
||||
$session['qr_code'] = $session['qr_code'] ? Security::decrypt($session['qr_code']) : null;
|
||||
}
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all WhatsApp sessions for a company
|
||||
*/
|
||||
public static function findAllByCompany(int $companyId): array
|
||||
{
|
||||
$sessions = Database::select(
|
||||
"SELECT * FROM " . static::$table . " WHERE company_id = ? ORDER BY id ASC",
|
||||
[$companyId]
|
||||
);
|
||||
|
||||
foreach ($sessions as &$session) {
|
||||
$session['phone'] = $session['phone'] ? Security::decrypt($session['phone']) : null;
|
||||
$session['qr_code'] = $session['qr_code'] ? Security::decrypt($session['qr_code']) : null;
|
||||
}
|
||||
|
||||
return $sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a session by session_key (used by webhooks)
|
||||
*/
|
||||
|
||||
48
backend/app/Services/TTSService.php
Normal file
48
backend/app/Services/TTSService.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class TTSService
|
||||
{
|
||||
/**
|
||||
* Convert text to speech using Google Translate TTS API (Free/No authentication required).
|
||||
* Returns the MP3 audio file content base64-encoded.
|
||||
*
|
||||
* @param string $text Arabic text to convert (e.g. "رمز التحقق الخاص بك هو 5 4 2 1")
|
||||
* @return string|null Base64 encoded audio string or null on failure
|
||||
*/
|
||||
public static function textToSpeechArabic(string $text): ?string
|
||||
{
|
||||
try {
|
||||
$encodedText = urlencode($text);
|
||||
$url = "https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&tl=ar&q={$encodedText}";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
|
||||
$audioBinary = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200 || empty($audioBinary)) {
|
||||
error_log("[TTS Service Error] Failed to fetch audio from Google TTS. HTTP Code: {$httpCode}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify if the response is actually audio
|
||||
if (strpos(strtolower($contentType), 'audio') === false && strlen($audioBinary) < 1000) {
|
||||
error_log("[TTS Service Error] Response content-type was not audio: {$contentType}. Body: " . substr($audioBinary, 0, 100));
|
||||
return null;
|
||||
}
|
||||
|
||||
return base64_encode($audioBinary);
|
||||
} catch (\Exception $e) {
|
||||
error_log("[TTS Service Exception] " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ try {
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
echo "=== 1. Subscription Plans ===\n";
|
||||
$plans = Database::selectAll("SELECT * FROM subscription_plans");
|
||||
$plans = Database::select("SELECT * FROM subscription_plans");
|
||||
if (empty($plans)) {
|
||||
echo "No subscription plans found.\n";
|
||||
} else {
|
||||
@@ -22,7 +22,7 @@ try {
|
||||
}
|
||||
|
||||
echo "\n=== 2. Companies ===\n";
|
||||
$companies = Database::selectAll("SELECT * FROM companies LIMIT 10");
|
||||
$companies = Database::select("SELECT * FROM companies LIMIT 10");
|
||||
if (empty($companies)) {
|
||||
echo "No companies found.\n";
|
||||
} else {
|
||||
@@ -32,7 +32,7 @@ try {
|
||||
}
|
||||
|
||||
echo "\n=== 3. Active Company Subscriptions ===\n";
|
||||
$subs = Database::selectAll("
|
||||
$subs = Database::select("
|
||||
SELECT cs.*, p.name as plan_name
|
||||
FROM company_subscriptions cs
|
||||
JOIN subscription_plans p ON cs.plan_id = p.id
|
||||
@@ -46,7 +46,7 @@ try {
|
||||
}
|
||||
|
||||
echo "\n=== 4. WooCommerce Stores ===\n";
|
||||
$stores = Database::selectAll("SELECT id, company_id, store_url, is_active, webhook_secret FROM woocommerce_stores");
|
||||
$stores = Database::select("SELECT id, company_id, store_url, is_active, webhook_secret FROM woocommerce_stores");
|
||||
if (empty($stores)) {
|
||||
echo "No WooCommerce stores found.\n";
|
||||
} else {
|
||||
|
||||
1027
backend/public/admin.html
Normal file
1027
backend/public/admin.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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">×</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">×</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;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ $router->get('/', function ($request, $response) {
|
||||
exit;
|
||||
});
|
||||
|
||||
// Serve admin.html super admin panel on /admin path
|
||||
$router->get('/admin', function ($request, $response) {
|
||||
$response->setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
$response->sendHeaders();
|
||||
readfile(__DIR__ . '/admin.html');
|
||||
exit;
|
||||
});
|
||||
|
||||
// Health Check — no php_version or environment in production to avoid info disclosure
|
||||
$router->get('/api/health', function ($request, $response) {
|
||||
$response->json([
|
||||
@@ -43,12 +51,28 @@ $router->post('/api/auth/register', [\App\Controllers\AuthController::class, 're
|
||||
$router->post('/api/auth/login', [\App\Controllers\AuthController::class, 'login'], [\App\Middlewares\RateLimitMiddleware::class]);
|
||||
$router->get('/api/auth/me', [\App\Controllers\AuthController::class, 'me'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
|
||||
// WhatsApp Gateway Routes
|
||||
// WhatsApp Gateway & Multi-Session Routes
|
||||
$router->get('/api/whatsapp/status', [\App\Controllers\WhatsAppController::class, 'status'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/whatsapp/qr', [\App\Controllers\WhatsAppController::class, 'requestQr'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);
|
||||
$router->post('/api/whatsapp/disconnect', [\App\Controllers\WhatsAppController::class, 'disconnect'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->get('/api/whatsapp/sessions', [\App\Controllers\WhatsAppController::class, 'listSessions'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/whatsapp/sessions', [\App\Controllers\WhatsAppController::class, 'createSession'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);
|
||||
$router->delete('/api/whatsapp/sessions', [\App\Controllers\WhatsAppController::class, 'deleteSession'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/whatsapp/webhook', [\App\Controllers\WhatsAppController::class, 'webhook']); // No AuthMiddleware (Protected by WEBHOOK_SECRET internally)
|
||||
|
||||
// Customer Service Agents (Staff) Routes
|
||||
$router->get('/api/staff', [\App\Controllers\StaffController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/staff', [\App\Controllers\StaffController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);
|
||||
$router->delete('/api/staff', [\App\Controllers\StaffController::class, 'delete'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->put('/api/staff/assign', [\App\Controllers\StaffController::class, 'assignSession'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
|
||||
// Text and Voice OTP Verification Routes
|
||||
$router->post('/api/otp/send', [\App\Controllers\OTPController::class, 'send'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);
|
||||
|
||||
// Super Admin Routes
|
||||
$router->get('/api/admin/stats', [\App\Controllers\SuperAdminController::class, 'getStats'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/admin/companies/subscribe', [\App\Controllers\SuperAdminController::class, 'subscribeCompany'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
|
||||
// Phase 4 & 5: CRM, Templates & Campaigns Routes
|
||||
$router->get('/api/contacts', [\App\Controllers\ContactController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/contacts', [\App\Controllers\ContactController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);
|
||||
|
||||
Reference in New Issue
Block a user