Deploy: 2026-05-25 00:29:42

This commit is contained in:
Hamza-Ayed
2026-05-25 00:29:42 +03:00
parent b20f457eaf
commit 7359206eb3
14 changed files with 1126 additions and 213 deletions

View File

@@ -0,0 +1,285 @@
<?php
namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Models\MetaSession;
use App\Models\MessageLog;
use App\Models\Contact;
use App\Models\ChatbotRule;
use App\Services\MetaService;
use App\Services\GeminiService;
class MetaWebhookController extends BaseController
{
/**
* Verify Meta Webhook Token (GET /api/webhooks/meta)
*/
public function verify(Request $request, Response $response)
{
$mode = $_GET['hub_mode'] ?? $_GET['hub.mode'] ?? '';
$token = $_GET['hub_verify_token'] ?? $_GET['hub.verify_token'] ?? '';
$challenge = $_GET['hub_challenge'] ?? $_GET['hub.challenge'] ?? '';
$verifyToken = getenv('META_WEBHOOK_VERIFY_TOKEN') ?: 'nabeh_meta_secret_token';
if ($mode === 'subscribe' && $token === $verifyToken) {
$response->status(200)->setHeader('Content-Type', 'text/plain');
echo $challenge;
exit;
} else {
$response->status(403)->json(['error' => 'Verification failed']);
}
}
/**
* Handle incoming Meta Webhook events (POST /api/webhooks/meta)
*/
public function webhook(Request $request, Response $response)
{
$rawPayload = file_get_contents('php://input');
$payload = json_decode($rawPayload, true);
if (!$payload) {
$response->status(400)->json(['error' => 'Invalid JSON payload']);
return;
}
$object = $payload['object'] ?? '';
if ($object !== 'page' && $object !== 'instagram') {
$response->status(200)->json(['status' => 'ignored', 'reason' => 'unsupported object type']);
return;
}
$channelType = ($object === 'page') ? 'messenger' : 'instagram';
$entries = $payload['entry'] ?? [];
foreach ($entries as $entry) {
$pageId = $entry['id'] ?? '';
$messaging = $entry['messaging'] ?? [];
$session = MetaSession::findByPageId($pageId, $channelType);
if (!$session || $session['status'] !== 'connected') {
continue;
}
foreach ($messaging as $messageEvent) {
$senderId = $messageEvent['sender']['id'] ?? '';
$message = $messageEvent['message'] ?? [];
if (empty($senderId) || empty($message)) {
continue;
}
// Deduplicate incoming messages
$mid = $message['mid'] ?? '';
if (!empty($mid)) {
$alreadyLogged = \App\Core\Database::selectOne(
"SELECT id FROM messages_log WHERE whatsapp_message_id = ? LIMIT 1",
[$mid]
);
if ($alreadyLogged) {
continue;
}
}
$text = $message['text'] ?? '';
if (empty($text)) {
continue;
}
// 1. Find or create CRM Contact
$contact = Contact::findByPhone($session['company_id'], $senderId);
if (!$contact) {
$prefix = ($channelType === 'messenger') ? 'FB-' : 'IG-';
$contactName = $prefix . substr($senderId, -6);
try {
Contact::createSecure([
'company_id' => $session['company_id'],
'name' => $contactName,
'phone' => $senderId,
'notes' => 'Auto-created via Meta webhook'
]);
} catch (\PDOException $e) {
// Ignore duplicate contact error if created concurrently
}
}
// 2. Log inbound message in history log
MessageLog::logMessage([
'company_id' => $session['company_id'],
'session_id' => null,
'meta_session_id' => $session['id'],
'contact_phone' => $senderId,
'direction' => 'inbound',
'message_type' => 'text',
'message_body' => $text,
'whatsapp_message_id' => $mid,
'status' => 'read'
]);
// 3. Trigger chatbot rules auto-reply in background
$this->triggerMetaAutoReply($session, $senderId, $text);
}
}
$response->json(['status' => 'success']);
}
/**
* Trigger auto-reply based on keyword rules or Gemini AI
*/
private function triggerMetaAutoReply(array $session, string $senderId, string $incomingText)
{
$companyId = (int)$session['company_id'];
$rule = ChatbotRule::findActiveForRule($companyId);
if (!$rule || !$rule['is_active']) {
return;
}
$replyText = '';
if ($rule['trigger_type'] === 'keyword') {
$keywords = array_map('trim', explode(',', strtolower($rule['keyword'])));
$incomingLower = strtolower(trim($incomingText));
$matched = false;
foreach ($keywords as $kw) {
if ($kw !== '' && strpos($incomingLower, $kw) !== false) {
$matched = true;
break;
}
}
if ($matched) {
$replyText = $rule['ai_prompt'];
}
} elseif ($rule['trigger_type'] === 'gemini_ai') {
$configuredGeminiKey = !empty($rule['gemini_api_key']) ? $rule['gemini_api_key'] : null;
$apiKey = GeminiService::getGeminiApiKey($configuredGeminiKey);
if (empty($apiKey)) {
error_log("[Meta Chatbot Warning] Gemini API Key is not set.");
return;
}
$systemPrompt = $rule['ai_prompt'] ?: 'You are a helpful customer support assistant.';
// Enforce language matching rule
$systemPrompt .= "\n\nIMPORTANT LANGUAGE RULE: Detect the language of the incoming message. If the incoming message is in English, you MUST reply in English. If the incoming message is in Arabic, you MUST reply in Arabic. Override any default language instruction to match the user's language.";
$replyText = GeminiService::generateResponse($apiKey, $systemPrompt, $incomingText);
}
if (!empty($replyText)) {
$success = MetaService::sendMessage($session['page_access_token'], $senderId, $replyText);
if ($success) {
MessageLog::logMessage([
'company_id' => $companyId,
'session_id' => null,
'meta_session_id' => $session['id'],
'contact_phone' => $senderId,
'direction' => 'outbound',
'message_type' => 'text',
'message_body' => $replyText,
'status' => 'sent'
]);
}
}
}
/**
* Get Meta connection status for current company (GET /api/meta/sessions)
*/
public function listSessions(Request $request, Response $response)
{
$sessions = MetaSession::findAllByCompany($request->company_id);
$response->json([
'status' => 'success',
'data' => $sessions
]);
}
/**
* Connect Facebook page or Instagram profile (POST /api/meta/sessions/connect)
*/
public function connectSession(Request $request, Response $response)
{
$body = $request->getBody();
$channelType = $body['channel_type'] ?? '';
$pageId = $body['page_id'] ?? '';
$pageName = $body['page_name'] ?? '';
$pageAccessToken = $body['page_access_token'] ?? '';
if (empty($channelType) || empty($pageId) || empty($pageName) || empty($pageAccessToken)) {
$response->status(400)->json([
'status' => 'error',
'message' => 'Missing channel_type, page_id, page_name, or page_access_token'
]);
return;
}
if ($channelType !== 'messenger' && $channelType !== 'instagram') {
$response->status(400)->json([
'status' => 'error',
'message' => 'Invalid channel_type'
]);
return;
}
try {
$id = MetaSession::connectSession(
$request->company_id,
$channelType,
$pageId,
$pageName,
$pageAccessToken
);
$response->json([
'status' => 'success',
'message' => 'Meta channel connected successfully',
'session_id' => $id
]);
} catch (\Exception $e) {
$response->status(500)->json([
'status' => 'error',
'message' => 'Failed to connect: ' . $e->getMessage()
]);
}
}
/**
* Disconnect Meta session (DELETE /api/meta/sessions)
*/
public function deleteSession(Request $request, Response $response)
{
$body = $request->getBody();
$sessionId = $body['session_id'] ?? null;
if (!$sessionId) {
$response->status(400)->json([
'status' => 'error',
'message' => 'Missing session_id'
]);
return;
}
$session = MetaSession::find($sessionId);
if (!$session || (int)$session['company_id'] !== (int)$request->company_id) {
$response->status(404)->json([
'status' => 'error',
'message' => 'Session not found'
]);
return;
}
MetaSession::delete($sessionId);
$response->json([
'status' => 'success',
'message' => 'Meta channel disconnected successfully'
]);
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Models;
use App\Core\Security;
use App\Core\Database;
/**
* MetaSession Model
* Handles the meta_sessions table with encryption for page_access_token.
*/
class MetaSession extends BaseModel
{
protected static string $table = 'meta_sessions';
/**
* Get all connected Meta 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['page_access_token'] = $session['page_access_token'] ? Security::decrypt($session['page_access_token']) : '';
}
return $sessions;
}
/**
* Find a specific session by page_id and channel_type (messenger/instagram)
*/
public static function findByPageId(string $pageId, string $channelType)
{
$session = Database::selectOne(
"SELECT * FROM " . static::$table . " WHERE page_id = ? AND channel_type = ? LIMIT 1",
[$pageId, $channelType]
);
if ($session) {
$session['page_access_token'] = $session['page_access_token'] ? Security::decrypt($session['page_access_token']) : '';
}
return $session;
}
/**
* Save/Connect Meta session
*/
public static function connectSession(int $companyId, string $channelType, string $pageId, string $pageName, string $pageAccessToken): int
{
$existing = Database::selectOne(
"SELECT id FROM " . static::$table . " WHERE page_id = ? AND channel_type = ? LIMIT 1",
[$pageId, $channelType]
);
$encryptedToken = Security::encrypt($pageAccessToken);
if ($existing) {
static::update($existing['id'], [
'company_id' => $companyId,
'page_name' => $pageName,
'page_access_token' => $encryptedToken,
'status' => 'connected'
]);
return (int)$existing['id'];
} else {
return (int)static::create([
'company_id' => $companyId,
'channel_type' => $channelType,
'page_id' => $pageId,
'page_name' => $pageName,
'page_access_token' => $encryptedToken,
'status' => 'connected'
]);
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Services;
/**
* Meta Service
* Wraps communication with Facebook Graph API for Messenger and Instagram Business.
*/
class MetaService
{
/**
* Send a text message response to Meta Graph API
*/
public static function sendMessage(string $pageAccessToken, string $recipientPsid, string $messageText): bool
{
$url = "https://graph.facebook.com/v20.0/me/messages?access_token=" . urlencode($pageAccessToken);
$payload = json_encode([
'recipient' => ['id' => $recipientPsid],
'messaging_type' => 'RESPONSE',
'message' => ['text' => $messageText]
]);
$ch = curl_init($url);
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'
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
error_log("[MetaService Error] Failed to send message. HTTP Code: " . $httpCode . ", Response: " . $response);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,54 @@
<?php
require 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
try {
$pdo = new PDO(
"mysql:host=" . $_ENV['DB_HOST'] . ";dbname=" . $_ENV['DB_NAME'],
$_ENV['DB_USER'],
$_ENV['DB_PASS']
);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "=== Running Database Migrations: Meta Channel Integration ===\n";
// 1. Create meta_sessions table
$createSessionsTableSql = "
CREATE TABLE IF NOT EXISTS `meta_sessions` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`company_id` INT NOT NULL,
`channel_type` ENUM('messenger', 'instagram') NOT NULL,
`page_id` VARCHAR(255) NOT NULL,
`page_name` VARCHAR(255) NOT NULL,
`page_access_token` TEXT NOT NULL,
`status` ENUM('connected', 'disconnected') DEFAULT 'connected',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`company_id`) REFERENCES `companies`(`id`) ON DELETE CASCADE,
UNIQUE KEY `page_channel_unique` (`page_id`, `channel_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
";
$pdo->exec($createSessionsTableSql);
echo "✅ Table 'meta_sessions' verified/created.\n";
// 2. Make session_id column in messages_log nullable
// First check current column definition or just modify it
$pdo->exec("ALTER TABLE `messages_log` MODIFY COLUMN `session_id` INT NULL");
echo "✅ Modified 'session_id' in 'messages_log' to be nullable.\n";
// 3. Add meta_session_id column if not exists
$result = $pdo->query("SHOW COLUMNS FROM `messages_log` LIKE 'meta_session_id'");
if ($result->rowCount() === 0) {
$pdo->exec("ALTER TABLE `messages_log` ADD COLUMN `meta_session_id` INT NULL AFTER `session_id`");
$pdo->exec("ALTER TABLE `messages_log` ADD CONSTRAINT `fk_msg_meta_session` FOREIGN KEY (`meta_session_id`) REFERENCES `meta_sessions`(`id`) ON DELETE CASCADE");
echo "✅ Added 'meta_session_id' column and foreign key constraint to 'messages_log'.\n";
} else {
echo " Column 'meta_session_id' already exists in 'messages_log'. Skipping.\n";
}
echo "Migration completed successfully!\n";
} catch (PDOException $e) {
echo "❌ Database error: " . $e->getMessage() . "\n";
}

View File

@@ -96,6 +96,13 @@ $router->post('/api/whatsapp/sessions', [\App\Controllers\WhatsAppController::
$router->delete('/api/whatsapp/sessions', [\App\Controllers\WhatsAppController::class, 'deleteSession'], [\App\Middlewares\AuthMiddleware::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) $router->post('/api/whatsapp/webhook', [\App\Controllers\WhatsAppController::class, 'webhook']); // No AuthMiddleware (Protected by WEBHOOK_SECRET internally)
// Meta Channel Integration & Multi-Session Routes
$router->get('/api/meta/sessions', [\App\Controllers\MetaWebhookController::class, 'listSessions'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/meta/sessions/connect', [\App\Controllers\MetaWebhookController::class, 'connectSession'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);
$router->delete('/api/meta/sessions', [\App\Controllers\MetaWebhookController::class, 'deleteSession'], [\App\Middlewares\AuthMiddleware::class]);
$router->get('/api/webhooks/meta', [\App\Controllers\MetaWebhookController::class, 'verify']);
$router->post('/api/webhooks/meta', [\App\Controllers\MetaWebhookController::class, 'webhook']);
// Customer Service Agents (Staff) Routes // Customer Service Agents (Staff) Routes
$router->get('/api/staff', [\App\Controllers\StaffController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]); $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->post('/api/staff', [\App\Controllers\StaffController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);

View File

@@ -42,4 +42,13 @@ class ApiConstants {
// English: The endpoint to approve a pending company subscription. // English: The endpoint to approve a pending company subscription.
// Arabic: نقطة النهاية للموافقة على اشتراك معلق للشركة. // Arabic: نقطة النهاية للموافقة على اشتراك معلق للشركة.
static const String approveBillingEndpoint = '/admin/companies/approve-billing'; static const String approveBillingEndpoint = '/admin/companies/approve-billing';
// English: The endpoint to list connected Meta sessions.
static const String metaSessionsEndpoint = '/meta/sessions';
// English: The endpoint to connect a new Meta channel session.
static const String metaConnectEndpoint = '/meta/sessions/connect';
// English: The endpoint to disconnect a Meta channel session.
static const String metaDisconnectEndpoint = '/meta/sessions';
} }

View File

@@ -176,4 +176,52 @@ class ApiService {
}), }),
); );
} }
// English: Fetch Meta sessions connection status.
Future<http.Response> getMetaSessions(String token) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.metaSessionsEndpoint}');
return await _client.get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);
}
// English: Connect a new Facebook page or Instagram channel.
Future<http.Response> connectMetaSession(String token, String channelType, String pageId, String pageName, String pageAccessToken) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.metaConnectEndpoint}');
return await _client.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode({
'channel_type': channelType,
'page_id': pageId,
'page_name': pageName,
'page_access_token': pageAccessToken,
}),
);
}
// English: Disconnect a Meta page or Instagram channel.
Future<http.Response> disconnectMetaSession(String token, int sessionId) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.metaDisconnectEndpoint}');
return await _client.delete(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode({
'session_id': sessionId,
}),
);
}
} }

View File

@@ -6,6 +6,7 @@ import 'models/contact_model.dart';
import 'models/plan_model.dart'; import 'models/plan_model.dart';
import 'models/super_admin_stats_model.dart'; import 'models/super_admin_stats_model.dart';
import 'models/whatsapp_status_model.dart'; import 'models/whatsapp_status_model.dart';
import 'models/meta_session_model.dart';
class DashboardRepository { class DashboardRepository {
final ApiService _apiService = ApiService(); final ApiService _apiService = ApiService();
@@ -157,4 +158,42 @@ class DashboardRepository {
throw Exception(error); throw Exception(error);
} }
} }
// English: Load connected Meta sessions.
Future<List<MetaSessionModel>> getMetaSessions() async {
final token = await _getToken();
final response = await _apiService.getMetaSessions(token);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final list = decoded['data'] as List<dynamic>? ?? [];
return list.map((item) => MetaSessionModel.fromJson(item as Map<String, dynamic>)).toList();
} else {
throw Exception('فشل في جلب جلسات ميتا المتصلة');
}
}
// English: Connect a new Facebook Page or Instagram channel.
Future<void> connectMetaSession(String channelType, String pageId, String pageName, String pageAccessToken) async {
final token = await _getToken();
final response = await _apiService.connectMetaSession(token, channelType, pageId, pageName, pageAccessToken);
if (response.statusCode != 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final error = decoded['message'] as String? ?? 'فشل في ربط القناة';
throw Exception(error);
}
}
// English: Disconnect a Meta page or Instagram channel.
Future<void> disconnectMetaSession(int sessionId) async {
final token = await _getToken();
final response = await _apiService.disconnectMetaSession(token, sessionId);
if (response.statusCode != 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final error = decoded['message'] as String? ?? 'فشل في قطع اتصال القناة';
throw Exception(error);
}
}
} }

View File

@@ -0,0 +1,48 @@
// English: Model representing a connected Meta session (Facebook Page or Instagram Business Profile)
class MetaSessionModel {
final int id;
final int companyId;
final String channelType;
final String pageId;
final String pageName;
final String status;
final String? createdAt;
final String? updatedAt;
MetaSessionModel({
required this.id,
required this.companyId,
required this.channelType,
required this.pageId,
required this.pageName,
required this.status,
this.createdAt,
this.updatedAt,
});
factory MetaSessionModel.fromJson(Map<String, dynamic> json) {
return MetaSessionModel(
id: json['id'] as int,
companyId: json['company_id'] as int,
channelType: json['channel_type'] as String,
pageId: json['page_id'] as String,
pageName: json['page_name'] as String,
status: json['status'] as String,
createdAt: json['created_at'] as String?,
updatedAt: json['updated_at'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'company_id': companyId,
'channel_type': channelType,
'page_id': pageId,
'page_name': pageName,
'status': status,
'created_at': createdAt,
'updated_at': updatedAt,
};
}
}

View File

@@ -30,7 +30,12 @@ class DashboardCubit extends Cubit<DashboardState> {
switch (tab) { switch (tab) {
case DashboardTab.whatsapp: case DashboardTab.whatsapp:
final status = await _repository.getWhatsAppStatus(); final status = await _repository.getWhatsAppStatus();
emit(state.copyWith(whatsappStatus: status, isLoading: false)); final meta = await _repository.getMetaSessions();
emit(state.copyWith(
whatsappStatus: status,
metaSessions: meta,
isLoading: false,
));
break; break;
case DashboardTab.billing: case DashboardTab.billing:
final plans = await _repository.getPlans(); final plans = await _repository.getPlans();
@@ -123,4 +128,30 @@ class DashboardCubit extends Cubit<DashboardState> {
emit(state.copyWith(errorMessage: cleanMsg, isLoading: false)); emit(state.copyWith(errorMessage: cleanMsg, isLoading: false));
} }
} }
// English: Connect a Facebook Page or Instagram channel and refresh sessions.
Future<void> connectMetaSession(String channelType, String pageId, String pageName, String pageAccessToken) async {
emit(state.copyWith(isLoading: true));
try {
await _repository.connectMetaSession(channelType, pageId, pageName, pageAccessToken);
final meta = await _repository.getMetaSessions();
emit(state.copyWith(metaSessions: meta, isLoading: false));
} catch (e) {
final cleanMsg = e.toString().replaceAll('Exception: ', '');
emit(state.copyWith(errorMessage: cleanMsg, isLoading: false));
}
}
// English: Disconnect a Meta page/profile connection and refresh sessions.
Future<void> disconnectMetaSession(int sessionId) async {
emit(state.copyWith(isLoading: true));
try {
await _repository.disconnectMetaSession(sessionId);
final meta = await _repository.getMetaSessions();
emit(state.copyWith(metaSessions: meta, isLoading: false));
} catch (e) {
final cleanMsg = e.toString().replaceAll('Exception: ', '');
emit(state.copyWith(errorMessage: cleanMsg, isLoading: false));
}
}
} }

View File

@@ -4,6 +4,7 @@ import '../../data/models/contact_model.dart';
import '../../data/models/plan_model.dart'; import '../../data/models/plan_model.dart';
import '../../data/models/super_admin_stats_model.dart'; import '../../data/models/super_admin_stats_model.dart';
import '../../data/models/whatsapp_status_model.dart'; import '../../data/models/whatsapp_status_model.dart';
import '../../data/models/meta_session_model.dart';
// English: Enum representing the 9 tabs in the Nabeh web/mobile dashboard. // English: Enum representing the 9 tabs in the Nabeh web/mobile dashboard.
// Arabic: قائمة تعداد تمثل الأبواب التسعة في لوحة تحكم نبيه على الويب والهاتف. // Arabic: قائمة تعداد تمثل الأبواب التسعة في لوحة تحكم نبيه على الويب والهاتف.
@@ -39,6 +40,7 @@ class DashboardState extends Equatable {
final List<ContactModel> contacts; final List<ContactModel> contacts;
final List<ChatbotRuleModel> chatbotRules; final List<ChatbotRuleModel> chatbotRules;
final SuperAdminStatsModel? superAdminStats; final SuperAdminStatsModel? superAdminStats;
final List<MetaSessionModel> metaSessions;
const DashboardState({ const DashboardState({
required this.activeTab, required this.activeTab,
@@ -49,6 +51,7 @@ class DashboardState extends Equatable {
this.contacts = const [], this.contacts = const [],
this.chatbotRules = const [], this.chatbotRules = const [],
this.superAdminStats, this.superAdminStats,
this.metaSessions = const [],
}); });
// English: Helper copyWith constructor to copy immutable state data safely. // English: Helper copyWith constructor to copy immutable state data safely.
@@ -62,6 +65,7 @@ class DashboardState extends Equatable {
List<ContactModel>? contacts, List<ContactModel>? contacts,
List<ChatbotRuleModel>? chatbotRules, List<ChatbotRuleModel>? chatbotRules,
SuperAdminStatsModel? superAdminStats, SuperAdminStatsModel? superAdminStats,
List<MetaSessionModel>? metaSessions,
}) { }) {
return DashboardState( return DashboardState(
activeTab: activeTab ?? this.activeTab, activeTab: activeTab ?? this.activeTab,
@@ -72,6 +76,7 @@ class DashboardState extends Equatable {
contacts: contacts ?? this.contacts, contacts: contacts ?? this.contacts,
chatbotRules: chatbotRules ?? this.chatbotRules, chatbotRules: chatbotRules ?? this.chatbotRules,
superAdminStats: superAdminStats ?? this.superAdminStats, superAdminStats: superAdminStats ?? this.superAdminStats,
metaSessions: metaSessions ?? this.metaSessions,
); );
} }
@@ -85,5 +90,6 @@ class DashboardState extends Equatable {
contacts, contacts,
chatbotRules, chatbotRules,
superAdminStats, superAdminStats,
metaSessions,
]; ];
} }

View File

@@ -10,7 +10,7 @@ import '../widgets/chatbot_view.dart';
import '../widgets/contacts_view.dart'; import '../widgets/contacts_view.dart';
import '../widgets/simple_placeholder_view.dart'; import '../widgets/simple_placeholder_view.dart';
import '../widgets/super_admin_view.dart'; import '../widgets/super_admin_view.dart';
import '../widgets/whatsapp_view.dart'; import '../widgets/channels_view.dart';
class DashboardScreen extends StatelessWidget { class DashboardScreen extends StatelessWidget {
final UserModel user; final UserModel user;
@@ -99,10 +99,10 @@ class DashboardScreen extends StatelessWidget {
), ),
_buildDrawerItem( _buildDrawerItem(
context, context,
'📱 اتصال الواتساب', '📱 قنوات الاتصال',
DashboardTab.whatsapp, DashboardTab.whatsapp,
state.activeTab, state.activeTab,
Icons.phone_android, Icons.contact_mail_outlined,
), ),
_buildDrawerItem( _buildDrawerItem(
context, context,
@@ -228,7 +228,7 @@ class DashboardScreen extends StatelessWidget {
case DashboardTab.superAdmin: case DashboardTab.superAdmin:
return 'المشرف العام - نبيه'; return 'المشرف العام - نبيه';
case DashboardTab.whatsapp: case DashboardTab.whatsapp:
return 'اتصال الواتساب'; return 'قنوات الاتصال';
case DashboardTab.billing: case DashboardTab.billing:
return 'الباقات والاشتراكات'; return 'الباقات والاشتراكات';
case DashboardTab.contacts: case DashboardTab.contacts:
@@ -251,8 +251,9 @@ class DashboardScreen extends StatelessWidget {
case DashboardTab.superAdmin: case DashboardTab.superAdmin:
return SuperAdminView(stats: state.superAdminStats); return SuperAdminView(stats: state.superAdminStats);
case DashboardTab.whatsapp: case DashboardTab.whatsapp:
return WhatsAppView( return ChannelsView(
status: state.whatsappStatus, whatsappStatus: state.whatsappStatus,
metaSessions: state.metaSessions,
onRefresh: () => context.read<DashboardCubit>().refreshCurrentTab(), onRefresh: () => context.read<DashboardCubit>().refreshCurrentTab(),
); );
case DashboardTab.billing: case DashboardTab.billing:

View File

@@ -0,0 +1,466 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/models/whatsapp_status_model.dart';
import '../../data/models/meta_session_model.dart';
import '../cubit/dashboard_cubit.dart';
class ChannelsView extends StatefulWidget {
final WhatsAppStatusModel? whatsappStatus;
final List<MetaSessionModel> metaSessions;
final VoidCallback onRefresh;
const ChannelsView({
super.key,
required this.whatsappStatus,
required this.metaSessions,
required this.onRefresh,
});
@override
State<ChannelsView> createState() => _ChannelsViewState();
}
class _ChannelsViewState extends State<ChannelsView> with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
_tabController = TabController(length: 3, vsync: this);
super.initState();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
// English: Open confirmation dialog to disconnect WhatsApp.
void _showWhatsAppDisconnectDialog(BuildContext context) {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: const Color(0xFF15102A),
title: const Text('قطع اتصال الواتساب', style: TextStyle(color: Colors.white)),
content: const Text(
'هل أنت متأكد من رغبتك في قطع اتصال الواتساب؟ سيؤدي ذلك إلى تعطيل الردود التلقائية لعملاء الواتساب.',
style: TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء', style: TextStyle(color: Colors.white60)),
),
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
context.read<DashboardCubit>().disconnectWhatsApp();
},
child: const Text('نعم، اقطع الاتصال', style: TextStyle(color: Colors.redAccent)),
),
],
);
},
);
}
// English: Open confirmation dialog to disconnect a Meta session.
void _showMetaDisconnectDialog(BuildContext context, MetaSessionModel session) {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: const Color(0xFF15102A),
title: Text(
session.channelType == 'messenger' ? 'قطع اتصال ماسنجر' : 'قطع اتصال إنستغرام',
style: const TextStyle(color: Colors.white),
),
content: Text(
'هل أنت متأكد من رغبتك في قطع الاتصال عن ${session.pageName}؟',
style: const TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء', style: TextStyle(color: Colors.white60)),
),
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
context.read<DashboardCubit>().disconnectMetaSession(session.id);
},
child: const Text('نعم، اقطع الاتصال', style: TextStyle(color: Colors.redAccent)),
),
],
);
},
);
}
// English: Show connect dialog for Facebook Page or Instagram Business Account.
void _showConnectMetaDialog(BuildContext context, String type) {
final pageNameController = TextEditingController();
final pageIdController = TextEditingController();
final tokenController = TextEditingController();
final formKey = GlobalKey<FormState>();
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: const Color(0xFF15102A),
title: Text(
type == 'messenger' ? 'ربط صفحة فيسبوك' : 'ربط حساب إنستغرام',
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: pageNameController,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: type == 'messenger' ? 'اسم الصفحة' : 'اسم الحساب',
labelStyle: const TextStyle(color: Colors.white60),
enabledBorder: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.white30)),
),
validator: (v) => (v == null || v.isEmpty) ? 'الرجاء إدخال الاسم' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: pageIdController,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: type == 'messenger' ? 'معرّف الصفحة (Page ID)' : 'معرّف الحساب (Account ID)',
labelStyle: const TextStyle(color: Colors.white60),
enabledBorder: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.white30)),
),
validator: (v) => (v == null || v.isEmpty) ? 'الرجاء إدخال المعرّف' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: tokenController,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
labelText: 'رمز الوصول للصفحة (Access Token)',
labelStyle: TextStyle(color: Colors.white60),
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.white30)),
),
validator: (v) => (v == null || v.isEmpty) ? 'الرجاء إدخال رمز الوصول' : null,
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء', style: TextStyle(color: Colors.white60)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.purpleAccent),
onPressed: () {
if (formKey.currentState!.validate()) {
Navigator.pop(dialogContext);
context.read<DashboardCubit>().connectMetaSession(
type,
pageIdController.text.trim(),
pageNameController.text.trim(),
tokenController.text.trim(),
);
}
},
child: const Text('ربط وتفعيل'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// English: Channel selector TabBar.
Container(
color: const Color(0xFF15102A),
child: TabBar(
controller: _tabController,
indicatorColor: Colors.purpleAccent,
labelColor: Colors.purpleAccent,
unselectedLabelColor: Colors.white60,
tabs: const [
Tab(icon: Icon(Icons.phone_android), text: 'واتساب'),
Tab(icon: Icon(Icons.facebook), text: 'فيسبوك ماسنجر'),
Tab(icon: Icon(Icons.camera_alt), text: 'إنستغرام'),
],
),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.75,
child: TabBarView(
controller: _tabController,
children: [
_buildWhatsAppTab(),
_buildMetaTab('messenger'),
_buildMetaTab('instagram'),
],
),
),
],
);
}
Widget _buildWhatsAppTab() {
final session = widget.whatsappStatus;
return SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'📱 إعدادات اتصال الواتساب',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
const Text(
'اربط حسابك مع بوابة واتساب لتفعيل الردود التلقائية وروبوت خدمة العملاء.',
style: TextStyle(color: Colors.white60, fontSize: 12),
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('حالة الجلسة الحالية', style: TextStyle(color: Colors.white70, fontSize: 14)),
_buildWhatsAppStatusBadge(session?.status ?? 'disconnected'),
],
),
const Divider(color: Colors.white10, height: 24),
_buildDetailsRow('اسم الجلسة', session?.name ?? 'WhatsApp Team'),
_buildDetailsRow('مفتاح التعريف', session?.sessionKey ?? 'لا يوجد'),
if (session?.phone != null) _buildDetailsRow('رقم الهاتف المرتبط', session!.phone!),
const SizedBox(height: 20),
if (session == null || session.status == 'disconnected') ...[
const Text(
'⚠️ الحساب غير متصل. يرجى توليد رمز الاستجابة السريعة ومسحه ضوئياً لتفعيل الخدمة.',
style: TextStyle(color: Colors.orangeAccent, fontSize: 12),
),
const SizedBox(height: 20),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purpleAccent,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
icon: const Icon(Icons.qr_code_scanner),
label: const Text('توليد رمز الاستجابة QR'),
onPressed: () {
context.read<DashboardCubit>().requestWhatsAppQr();
},
),
] else if (session.status == 'waiting_qr') ...[
const Text(
'🔍 رمز الاستجابة جاهز. افتح الواتساب واختر الأجهزة المرتبطة لمسح الرمز:',
style: TextStyle(color: Colors.yellowAccent, fontSize: 12),
),
const SizedBox(height: 20),
Center(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.qr_code_2, size: 180, color: Colors.black),
),
),
const SizedBox(height: 20),
] else if (session.status == 'connected') ...[
const Text(
'✅ الحساب متصل وجاهز للعمل. الردود التلقائية وروبوت خدمة العملاء نشطان.',
style: TextStyle(color: Colors.greenAccent, fontSize: 12),
),
const SizedBox(height: 20),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
icon: const Icon(Icons.link_off),
label: const Text('قطع الاتصال'),
onPressed: () => _showWhatsAppDisconnectDialog(context),
),
],
],
),
),
],
),
);
}
Widget _buildMetaTab(String type) {
final filtered = widget.metaSessions.where((s) => s.channelType == type).toList();
final title = (type == 'messenger') ? 'فيسبوك ماسنجر' : 'إنستغرام الأعمال';
final desc = (type == 'messenger')
? 'اربط صفحات فيسبوك الخاصة بك لتفعيل الرد الآلي والمحادثات مع العملاء.'
: 'اربط حسابات إنستغرام للأعمال لتفعيل روبوت المحادثة المخصص.';
return SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'💬 إعدادات $title',
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
Text(
desc,
style: const TextStyle(color: Colors.white60, fontSize: 12),
),
],
),
],
),
const SizedBox(height: 20),
if (filtered.isEmpty) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Icon(type == 'messenger' ? Icons.facebook : Icons.camera_alt, size: 48, color: Colors.white30),
const SizedBox(height: 12),
const Text('لا توجد قنوات مرتبطة حالياً', style: TextStyle(color: Colors.white70, fontSize: 14)),
const SizedBox(height: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purpleAccent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
icon: const Icon(Icons.add),
label: Text('ربط قناة $title new'),
onPressed: () => _showConnectMetaDialog(context, type),
),
],
),
),
] else ...[
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filtered.length,
itemBuilder: (context, index) {
final item = filtered[index];
return Card(
color: const Color(0xFF15102A),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.purpleAccent.withOpacity(0.1),
child: Icon(type == 'messenger' ? Icons.messenger_outline : Icons.camera_alt, color: Colors.purpleAccent),
),
title: Text(item.pageName, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
subtitle: Text('ID: ${item.pageId}', style: const TextStyle(color: Colors.white60, fontSize: 12)),
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
onPressed: () => _showMetaDisconnectDialog(context, item),
),
),
);
},
),
const SizedBox(height: 16),
Center(
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.purpleAccent,
side: const BorderSide(color: Colors.purpleAccent),
),
icon: const Icon(Icons.add),
label: const Text('ربط صفحة إضافية'),
onPressed: () => _showConnectMetaDialog(context, type),
),
),
],
],
),
);
}
Widget _buildDetailsRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(color: Colors.white70, fontSize: 13)),
Text(value, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildWhatsAppStatusBadge(String status) {
Color color;
String text;
switch (status) {
case 'connected':
color = Colors.green;
text = 'متصل';
break;
case 'waiting_qr':
color = Colors.orange;
text = 'بانتظار المسح';
break;
case 'connecting':
color = Colors.blue;
text = 'جاري الاتصال';
break;
default:
color = Colors.red;
text = 'غير متصل';
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color),
),
child: Text(text, style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold)),
);
}
}

View File

@@ -1,206 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/models/whatsapp_status_model.dart';
import '../cubit/dashboard_cubit.dart';
class WhatsAppView extends StatelessWidget {
final WhatsAppStatusModel? status;
final VoidCallback onRefresh;
const WhatsAppView({
super.key,
required this.status,
required this.onRefresh,
});
// English: Show a confirmation dialog before disconnecting.
// Arabic: عرض مربع حوار تأكيدي قبل قطع الاتصال لتجنب الإجراء المفاجئ.
void _showDisconnectDialog(BuildContext context) {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: const Color(0xFF15102A),
title: const Text('قطع اتصال الواتساب', style: TextStyle(color: Colors.white)),
content: const Text(
'هل أنت متأكد من رغبتك في قطع الاتصال؟ سيؤدي ذلك إلى تعطيل روبوت خدمة العملاء والردود التلقائية.',
style: TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء', style: TextStyle(color: Colors.white60)),
),
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
// English: Dispatch disconnectWhatsApp command to DashboardCubit.
// Arabic: استدعاء أمر قطع اتصال الواتساب في الكيوبيت.
context.read<DashboardCubit>().disconnectWhatsApp();
},
child: const Text('نعم، اقطع الاتصال', style: TextStyle(color: Colors.redAccent)),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final session = status;
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'📱 إعدادات اتصال الواتساب',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'اربط حسابك مع بوابة واتساب التابعة لنظام نبيه لتفعيل الردود التلقائية والتحقق.',
style: TextStyle(color: Colors.white60, fontSize: 13),
),
const SizedBox(height: 24),
// English: Display WhatsApp connection card containing session and status.
// Arabic: عرض بطاقة اتصال الواتساب التي تحتوي على الجلسة والحالة.
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'حالة الجلسة الحالية',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
_buildStatusBadge(session?.status ?? 'disconnected'),
],
),
const Divider(color: Colors.white10, height: 24),
_buildRow('اسم الجلسة', session?.name ?? 'WhatsApp Team'),
_buildRow('مفتاح التعريف', session?.sessionKey ?? 'لا يوجد'),
if (session?.phone != null) _buildRow('رقم الهاتف المرتبط', session!.phone!),
const SizedBox(height: 24),
// English: Render different elements depending on connection status.
// Arabic: عرض عناصر مختلفة حسب حالة الاتصال.
if (session == null || session.status == 'disconnected') ...[
const Text(
'⚠️ الحساب غير متصل. يرجى توليد رمز الاستجابة السريعة (QR Code) ومسحه ضوئياً لتفعيل الاتصال.',
style: TextStyle(color: Colors.orangeAccent, fontSize: 13),
),
const SizedBox(height: 20),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purpleAccent,
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
icon: const Icon(Icons.qr_code_scanner),
label: const Text('توليد رمز الاستجابة QR'),
onPressed: () {
// English: Dispatch requestWhatsAppQr command to DashboardCubit.
// Arabic: استدعاء أمر طلب رمز الاستجابة في الكيوبيت.
context.read<DashboardCubit>().requestWhatsAppQr();
},
),
] else if (session.status == 'waiting_qr') ...[
const Text(
'🔍 رمز الاستجابة جاهز للمسح. يرجى فتح الواتساب في هاتفك واختيار "الأجهزة المرتبطة" ثم مسح الرمز أدناه:',
style: TextStyle(color: Colors.yellowAccent, fontSize: 13),
),
const SizedBox(height: 20),
Center(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.qr_code_2, size: 200, color: Colors.black),
),
),
const SizedBox(height: 20),
] else if (session.status == 'connected') ...[
const Text(
'✅ الحساب متصل وجاهز للعمل. الردود التلقائية وروبوت خدمة العملاء نشطان الآن.',
style: TextStyle(color: Colors.greenAccent, fontSize: 13),
),
const SizedBox(height: 20),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
icon: const Icon(Icons.link_off),
label: const Text('قطع الاتصال'),
onPressed: () => _showDisconnectDialog(context),
),
],
],
),
),
],
),
);
}
Widget _buildRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(color: Colors.white70, fontSize: 13)),
Text(value, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildStatusBadge(String status) {
Color color;
String text;
switch (status) {
case 'connected':
color = Colors.green;
text = 'متصل';
break;
case 'waiting_qr':
color = Colors.orange;
text = 'بانتظار المسح';
break;
case 'connecting':
color = Colors.blue;
text = 'جاري الاتصال';
break;
default:
color = Colors.red;
text = 'غير متصل';
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color),
),
child: Text(text, style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold)),
);
}
}