Deploy: 2026-05-22 02:09:48

This commit is contained in:
Hamza-Ayed
2026-05-22 02:09:48 +03:00
parent e5c6c54ca0
commit f793092a17
7 changed files with 637 additions and 17 deletions

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Models\CompanyEndpoint;
class EndpointController extends BaseController
{
/**
* List all custom API endpoints for the company
*/
public function index(Request $request, Response $response)
{
$endpoints = CompanyEndpoint::findAllByCompany($request->company_id);
$response->json([
'status' => 'success',
'data' => $endpoints
]);
}
/**
* Create or update a custom API endpoint configuration
*/
public function store(Request $request, Response $response)
{
$errors = $this->validate($request, [
'name' => 'required',
'endpoint_url' => 'required',
'action_type' => 'required'
]);
if (!empty($errors)) {
$response->status(400)->json(['status' => 'error', 'errors' => $errors]);
return;
}
$body = $request->getBody();
$saveData = [
'company_id' => $request->company_id,
'name' => $body['name'],
'endpoint_url' => $body['endpoint_url'],
'action_type' => $body['action_type'],
'description' => $body['description'] ?? null,
'headers' => $body['headers'] ?? null
];
if (isset($body['id'])) {
$saveData['id'] = (int)$body['id'];
}
$id = CompanyEndpoint::saveSecure($saveData);
$response->json([
'status' => 'success',
'message' => 'Integration endpoint saved successfully',
'id' => $id
]);
}
/**
* Delete an API endpoint configuration
*/
public function delete(Request $request, Response $response)
{
$body = $request->getBody();
if (empty($body['id'])) {
$response->status(400)->json(['status' => 'error', 'message' => 'Missing endpoint ID']);
return;
}
// Verify ownership
$endpoint = CompanyEndpoint::find($body['id']);
if (!$endpoint || $endpoint['company_id'] !== $request->company_id) {
$response->status(404)->json(['status' => 'error', 'message' => 'Endpoint not found']);
return;
}
CompanyEndpoint::delete($body['id']);
$response->json([
'status' => 'success',
'message' => 'Integration endpoint deleted successfully'
]);
}
}

View File

@@ -335,8 +335,21 @@ class WhatsAppController extends BaseController
error_log("[Chatbot Warning] Gemini API Key is not set globally or for company " . $session['company_id']);
return;
}
// Dynamically fetch customer/driver info from configured endpoints if set
$infoContext = "";
$infoEndpoint = \App\Models\CompanyEndpoint::findByAction($session['company_id'], 'fetch_user_info');
if ($infoEndpoint && !empty($msgData['phone'])) {
$infoContext = $this->fetchUserInfoFromEndpoint($infoEndpoint, $msgData['phone']);
}
$systemPrompt = $rule['ai_prompt'] ?: 'You are a helpful customer support assistant.';
// Append real-time info context to Gemini system prompt
if (!empty($infoContext)) {
$systemPrompt .= "\n\n" . $infoContext;
}
// Enforce language matching rule dynamically
$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.";
@@ -368,8 +381,8 @@ class WhatsAppController extends BaseController
// Strip the tag from the final reply sent to user
$replyText = trim(str_replace($matches[0], '', $replyText));
// Call the payment verification API
$verificationResult = $this->verifyPaymentSlip($msgData['phone'], $jsonStr);
// Call the payment verification API (passing company_id)
$verificationResult = $this->verifyPaymentSlip($session['company_id'], $msgData['phone'], $jsonStr);
if ($verificationResult) {
$replyText .= "\n\n" . $verificationResult;
}
@@ -436,9 +449,9 @@ class WhatsAppController extends BaseController
}
/**
* Call external Entaleq API to verify payment slip
* Call external API to verify payment slip
*/
private function verifyPaymentSlip(string $phone, string $jsonStr): ?string
private function verifyPaymentSlip(int $companyId, string $phone, string $jsonStr): ?string
{
try {
$data = json_decode($jsonStr, true);
@@ -454,11 +467,17 @@ class WhatsAppController extends BaseController
return null;
}
// Determine API URL (default to localhost mock endpoint)
$apiUrl = getenv('ENTALEQ_PAYMENT_API_URL');
// Find configured endpoint for verify_payment
$endpoint = \App\Models\CompanyEndpoint::findByAction($companyId, 'verify_payment');
$apiUrl = $endpoint ? $endpoint['endpoint_url'] : null;
if (empty($apiUrl)) {
$appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/');
$apiUrl = $appUrl . '/api/external/verify-payment';
// Fallback to local default mock
$apiUrl = getenv('ENTALEQ_PAYMENT_API_URL');
if (empty($apiUrl)) {
$appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/');
$apiUrl = $appUrl . '/api/external/verify-payment';
}
}
$payload = json_encode([
@@ -468,14 +487,23 @@ class WhatsAppController extends BaseController
'method' => $method
]);
$headers = ['Content-Type: application/json'];
if ($endpoint && !empty($endpoint['headers'])) {
$customHeaders = json_decode($endpoint['headers'], true);
if (is_array($customHeaders)) {
foreach ($customHeaders as $key => $value) {
$headers[] = "$key: $value";
}
}
} else {
$headers[] = 'X-API-Key: ' . (getenv('ENTALEQ_API_KEY') ?: 'mock-key');
}
$ch = curl_init($apiUrl);
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-API-Key: ' . (getenv('ENTALEQ_API_KEY') ?: 'mock-key')
]);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
@@ -499,4 +527,45 @@ class WhatsAppController extends BaseController
return "⏳ تم استلام وصل الدفع بنجاح. يجري الآن مراجعته وتدقيقه يدوياً من قبل الإدارة الفنية لتأكيد شحن رصيدك.";
}
}
/**
* Fetch user/driver info from external API endpoint configured by the company
*/
private function fetchUserInfoFromEndpoint(array $endpoint, string $phone): string
{
try {
$apiUrl = $endpoint['endpoint_url'];
$payload = json_encode([
'phone' => $phone
]);
$headers = ['Content-Type: application/json'];
if (!empty($endpoint['headers'])) {
$customHeaders = json_decode($endpoint['headers'], true);
if (is_array($customHeaders)) {
foreach ($customHeaders as $key => $value) {
$headers[] = "$key: $value";
}
}
}
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 5); // Short timeout to avoid blocking chatbot flow
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200 && !empty($response)) {
return "\n\n[معلومات العميل المسترجعة من النظام الخارجي للمؤسسة:\n" . $response . "\nاستخدم هذه المعلومات للإجابة بدقة على أسئلة العميل المتعلقة بحسابه ورصيده وحالته]";
}
} catch (\Exception $e) {
error_log("[Fetch User Info Exception] " . $e->getMessage());
}
return "";
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Models;
use App\Core\Database;
/**
* CompanyEndpoint Model
* Handles configuration of dynamic integration endpoints for multi-tenant SaaS integration.
*/
class CompanyEndpoint extends BaseModel
{
protected static string $table = 'company_endpoints';
/**
* Find all endpoints for a company
*/
public static function findAllByCompany(int $companyId): array
{
self::ensureTableExists();
return Database::select(
"SELECT * FROM " . static::$table . " WHERE company_id = ? ORDER BY id DESC",
[$companyId]
);
}
/**
* Find a specific endpoint by action type for a company
*/
public static function findByAction(int $companyId, string $actionType): ?array
{
self::ensureTableExists();
return Database::selectOne(
"SELECT * FROM " . static::$table . " WHERE company_id = ? AND action_type = ? LIMIT 1",
[$companyId, $actionType]
);
}
/**
* Save or update an endpoint configuration
*/
public static function saveSecure(array $data)
{
self::ensureTableExists();
if (isset($data['id'])) {
$id = $data['id'];
unset($data['id']);
self::update($id, $data);
return $id;
} else {
return self::create($data);
}
}
/**
* Ensure the table exists dynamically
*/
public static function ensureTableExists(): void
{
static $checked = false;
if ($checked) return;
try {
Database::execute("
CREATE TABLE IF NOT EXISTS `company_endpoints` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`company_id` INT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`endpoint_url` VARCHAR(512) NOT NULL,
`action_type` VARCHAR(100) NOT NULL,
`description` TEXT NULL,
`headers` TEXT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_endpoint_company` (`company_id`),
INDEX `idx_endpoint_action` (`action_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
$checked = true;
} catch (\Exception $e) {
error_log("Failed to ensure company_endpoints table: " . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
<defs>
<!-- Background Gradient -->
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0b0d19" />
<stop offset="50%" stop-color="#111428" />
<stop offset="100%" stop-color="#070810" />
</linearGradient>
<!-- Icon Gradient -->
<linearGradient id="accentGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#06b6d4" />
<stop offset="50%" stop-color="#3b82f6" />
<stop offset="100%" stop-color="#6366f1" />
</linearGradient>
<!-- Glow Filter -->
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="12" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<!-- App Icon Background Rounded Rect -->
<rect width="512" height="512" rx="128" fill="url(#bgGrad)" stroke="rgba(255, 255, 255, 0.05)" stroke-width="4" />
<!-- Inner decorative tech circle -->
<circle cx="256" cy="256" r="220" fill="none" stroke="rgba(6, 182, 212, 0.1)" stroke-width="2" stroke-dasharray="10 15" />
<g filter="url(#glow)">
<!-- Speech Bubble Base -->
<path d="M 160 350
L 120 390
L 120 330
A 140 140 0 1 1 370 290
A 140 140 0 0 1 160 350 Z"
fill="none"
stroke="url(#accentGrad)"
stroke-width="16"
stroke-linejoin="round"
stroke-linecap="round" />
<!-- Intelligent Neural Nodes (Brain Paths inside Speech Bubble) -->
<!-- Center core node -->
<circle cx="256" cy="210" r="14" fill="#ffffff" />
<!-- Connections and branching nodes -->
<!-- Left top branch -->
<path d="M 256 210 L 200 170" fill="none" stroke="#06b6d4" stroke-width="8" stroke-linecap="round" />
<circle cx="200" cy="170" r="10" fill="#06b6d4" />
<!-- Right top branch -->
<path d="M 256 210 L 312 170" fill="none" stroke="#6366f1" stroke-width="8" stroke-linecap="round" />
<circle cx="312" cy="170" r="10" fill="#6366f1" />
<!-- Left bottom branch -->
<path d="M 256 210 L 190 240" fill="none" stroke="#06b6d4" stroke-width="8" stroke-linecap="round" />
<circle cx="190" cy="240" r="10" fill="#06b6d4" />
<!-- Right bottom branch -->
<path d="M 256 210 L 322 240" fill="none" stroke="#6366f1" stroke-width="8" stroke-linecap="round" />
<circle cx="322" cy="240" r="10" fill="#6366f1" />
<!-- Straight top vertical sensor -->
<path d="M 256 210 L 256 140" fill="none" stroke="#3b82f6" stroke-width="8" stroke-linecap="round" />
<circle cx="256" cy="140" r="10" fill="#3b82f6" />
<!-- Subtle horizontal link -->
<path d="M 200 170 A 70 70 0 0 1 312 170" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="4" stroke-dasharray="5 5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nabeh Hub — WhatsApp Gateway & CRM</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta name="description" content="Connect, manage, and scale your WhatsApp communication.">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -636,7 +637,10 @@
<div class="auth-wrapper">
<div class="auth-card">
<div class="brand-header">
<div class="brand-logo" id="main-brand-logo">Nabeh</div>
<div class="brand-logo" id="main-brand-logo" style="display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 0.75rem;">
<img src="/logo.svg" alt="Nabeh Logo" style="width: 48px; height: 48px; border-radius: 12px; box-shadow: 0 0 15px rgba(6, 182, 212, 0.4);">
<span style="font-family: 'Outfit', sans-serif; font-size: 2.2rem; font-weight: 800; background: linear-gradient(135deg, #fff 30%, var(--primary-accent) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.05em;">Nabeh</span>
</div>
<div class="brand-subtitle">WhatsApp Hub & Marketing Automation</div>
</div>
@@ -704,10 +708,13 @@
<!-- Main Dashboard View -->
<template x-if="isLoggedIn">
<div>
<div class="dashboard-header">
<div>
<h1 style="font-size: 1.8rem; margin-bottom: 0.25rem;">Nabeh Dashboard</h1>
<p class="text-muted" style="font-size: 0.9rem;">Connected to system: <span class="font-semibold" x-text="user.name"></span></p>
<div class="dashboard-header" style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 16px;">
<img src="/logo.svg" alt="Nabeh Logo" style="width: 52px; height: 52px; border-radius: 12px; box-shadow: 0 0 15px rgba(6, 182, 212, 0.35); border: 1px solid rgba(255, 255, 255, 0.08);">
<div>
<h1 style="font-size: 1.8rem; margin: 0 0 0.15rem 0;">Nabeh Dashboard</h1>
<p class="text-muted" style="font-size: 0.9rem; margin: 0;">Connected to system: <span class="font-semibold" x-text="user.name"></span></p>
</div>
</div>
<div class="user-info">
<div class="avatar" x-text="user.name.charAt(0).toUpperCase()"></div>
@@ -733,6 +740,9 @@
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'chatbot' }" @click="activeDashboardTab = 'chatbot'; fetchChatbotSettings()" id="nav-chatbot-btn">
<span>🤖</span> AI Chatbot Settings
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'integrations' }" @click="activeDashboardTab = 'integrations'; fetchEndpoints()" id="nav-integrations-btn">
<span>🔌</span> API Integrations
</button>
</div>
<!-- Right Dashboard Panels -->
@@ -1057,6 +1067,53 @@
</form>
</div>
<!-- Panel: API Integrations -->
<div class="panel" x-show="activeDashboardTab === 'integrations'" id="panel-integrations">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 style="font-size: 1.4rem; margin: 0;">API Endpoints Integration</h2>
<button class="btn btn-primary" style="width: auto;" @click="endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', headers: '' }; showAddEndpointModal = true" id="add-endpoint-btn">
+ Add API Integration
</button>
</div>
<p class="text-muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;">
Configure external web APIs for multi-tenant integrations. The chatbot can fetch user profiles or verify payment details dynamically.
</p>
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th>Integration Name</th>
<th>Action Type</th>
<th>Endpoint URL</th>
<th>Description</th>
<th style="width: 150px; text-align: center;">Actions</th>
</tr>
</thead>
<tbody>
<template x-for="endpoint in endpoints" :key="endpoint.id">
<tr>
<td class="font-semibold" x-text="endpoint.name"></td>
<td>
<span class="status-badge" :class="endpoint.action_type === 'verify_payment' ? 'badge-waiting_qr' : 'badge-connecting'" style="margin: 0; padding: 0.2rem 0.5rem; font-size: 0.75rem;" x-text="endpoint.action_type === 'verify_payment' ? 'Verify Payment' : 'Fetch User Info'"></span>
</td>
<td style="font-family: monospace; font-size: 0.85rem;" x-text="endpoint.endpoint_url"></td>
<td x-text="endpoint.description || 'No description provided.'"></td>
<td style="text-align: center; display: flex; gap: 0.5rem; justify-content: center; align-items: center;">
<button class="btn btn-secondary" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="editEndpoint(endpoint)" :id="'edit-endpoint-btn-' + endpoint.id">Edit</button>
<button class="btn btn-danger" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="deleteEndpoint(endpoint.id)" :id="'delete-endpoint-btn-' + endpoint.id">Delete</button>
</td>
</tr>
</template>
</tbody>
</table>
<template x-if="endpoints.length === 0">
<div class="empty-state" id="empty-endpoints-state">No API endpoints configured yet. Connect Intaleq or Salla integrations.</div>
</template>
</div>
</div>
</div>
</div>
@@ -1097,6 +1154,53 @@
</div>
</div>
<!-- Modal: Add/Edit Endpoint Integration -->
<div class="modal-overlay" x-show="showAddEndpointModal" id="modal-add-endpoint" style="display: none;">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title" x-text="endpointForm.id ? 'Edit API Endpoint Integration' : 'Add API Endpoint Integration'"></h3>
<button class="modal-close" @click="showAddEndpointModal = false">&times;</button>
</div>
<form @submit.prevent="submitAddEndpoint()" id="form-add-endpoint">
<div class="modal-body">
<div class="form-group">
<label class="form-label">Integration Name</label>
<input type="text" class="form-input" x-model="endpointForm.name" required placeholder="e.g. Intaleq Driver Lookup" id="add-endpoint-name">
</div>
<div class="form-group">
<label class="form-label">Endpoint URL</label>
<input type="url" class="form-input" x-model="endpointForm.endpoint_url" required placeholder="https://yourdomain.com/api/nabeh/action" id="add-endpoint-url">
</div>
<div class="form-group">
<label class="form-label">Action Type</label>
<select class="form-input" x-model="endpointForm.action_type" required id="add-endpoint-action-type">
<option value="verify_payment">Verify Payment (تحليل وتأكيد الدفع)</option>
<option value="fetch_user_info">Fetch User Info (جلب معلومات السائق أو العميل)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea class="form-input" x-model="endpointForm.description" placeholder="A brief explanation of what this endpoint does..." id="add-endpoint-description"></textarea>
</div>
<div class="form-group">
<label class="form-label">Custom HTTP Headers (JSON Object)</label>
<textarea class="form-input" style="font-family: monospace; font-size: 0.85rem; height: 120px;" x-model="endpointForm.headers" placeholder='{&#10; "Authorization": "Bearer YOUR_SECRET_TOKEN",&#10; "X-Custom-Source": "Nabeh App"&#10;}' id="add-endpoint-headers"></textarea>
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;">
Optionally define any authorization tokens or custom headers in JSON format.
</span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showAddEndpointModal = false">Cancel</button>
<button type="submit" class="btn btn-primary" style="width: auto;" :disabled="actionLoading">
<span x-show="!actionLoading" x-text="endpointForm.id ? 'Save Changes' : 'Create Integration'"></span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</div>
</form>
</div>
</div>
<!-- Modal: New Template -->
<div class="modal-overlay" x-show="showNewTemplateModal" id="modal-new-template" style="display: none;">
<div class="modal-card">
@@ -1231,6 +1335,18 @@
showAddContactModal: false,
showNewTemplateModal: false,
showLaunchCampaignModal: false,
showAddEndpointModal: false,
// Endpoint Form
endpointForm: {
id: null,
name: '',
endpoint_url: '',
action_type: 'verify_payment',
description: '',
headers: ''
},
endpoints: [],
// Forms
contactName: '',
@@ -1358,6 +1474,7 @@
this.contacts = [];
this.templates = [];
this.campaigns = [];
this.endpoints = [];
},
initializeDashboard() {
@@ -1471,6 +1588,94 @@
}
},
// Custom API Integrations CRUD
async fetchEndpoints() {
try {
const response = await fetch('/api/endpoints', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok) {
this.endpoints = data.data || [];
}
} catch (err) {
console.error('Error fetching endpoints:', err);
}
},
async submitAddEndpoint() {
this.actionLoading = true;
try {
if (this.endpointForm.headers && this.endpointForm.headers.trim()) {
try {
JSON.parse(this.endpointForm.headers);
} catch (jsonErr) {
alert('Invalid JSON in Custom Headers. Please verify format.');
this.actionLoading = false;
return;
}
}
const response = await fetch('/api/endpoints', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify(this.endpointForm)
});
const data = await response.json();
if (response.ok) {
this.showAddEndpointModal = false;
this.endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', headers: '' };
await this.fetchEndpoints();
} else {
alert(data.message || 'Failed to save integration endpoint.');
}
} catch (err) {
console.error('Error saving endpoint:', err);
} finally {
this.actionLoading = false;
}
},
editEndpoint(endpoint) {
this.endpointForm = {
id: endpoint.id,
name: endpoint.name,
endpoint_url: endpoint.endpoint_url,
action_type: endpoint.action_type,
description: endpoint.description || '',
headers: endpoint.headers || ''
};
this.showAddEndpointModal = true;
},
async deleteEndpoint(id) {
if (!confirm('Are you sure you want to delete this integration endpoint?')) return;
this.actionLoading = true;
try {
const response = await fetch('/api/endpoints', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({ id: id })
});
const data = await response.json();
if (response.ok) {
await this.fetchEndpoints();
} else {
alert(data.message || 'Failed to delete endpoint.');
}
} catch (err) {
console.error('Error deleting endpoint:', err);
} finally {
this.actionLoading = false;
}
},
// CRM List Fetchers
async fetchContacts() {
this.selectedContactIds = [];

View File

@@ -68,6 +68,38 @@ $router->get('/api/chatbot/rules', [\App\Controllers\ChatbotController::class, '
$router->post('/api/chatbot/rules',[\App\Controllers\ChatbotController::class, 'store'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/chatbot/generate-prompt-from-audio', [\App\Controllers\ChatbotController::class, 'generatePromptFromAudio'], [\App\Middlewares\AuthMiddleware::class]);
// Custom Integration Endpoints Routes (Phase 5)
$router->get('/api/endpoints', [\App\Controllers\EndpointController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/endpoints', [\App\Controllers\EndpointController::class, 'store'], [\App\Middlewares\AuthMiddleware::class]);
$router->delete('/api/endpoints', [\App\Controllers\EndpointController::class, 'delete'], [\App\Middlewares\AuthMiddleware::class]);
// Mock External API for Entaleq Driver Info (Used to fetch real-time driver data)
$router->post('/api/external/driver-info', function ($request, $response) {
$body = $request->getBody();
$phone = $body['phone'] ?? '';
if (empty($phone)) {
$response->status(400)->json([
'status' => 'error',
'message' => 'Missing phone number'
]);
return;
}
$response->json([
'status' => 'success',
'data' => [
'name' => 'أحمد الشريف',
'phone' => $phone,
'role' => 'سائق',
'status' => 'نشط',
'balance' => 75.25,
'vehicle' => 'تويوتا كامري 2023',
'trips_count' => 1420
]
]);
});
// Mock External API for Entaleq Payment Verification (Used to demo automated slip validation)
$router->post('/api/external/verify-payment', function ($request, $response) {
$body = $request->getBody();
@@ -107,5 +139,6 @@ $router->post('/api/external/verify-payment', function ($request, $response) {
]);
});
// 4. Dispatch the request
$router->dispatch($request, $response);

71
backend/public/logo.svg Normal file
View File

@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
<defs>
<!-- Background Gradient -->
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0b0d19" />
<stop offset="50%" stop-color="#111428" />
<stop offset="100%" stop-color="#070810" />
</linearGradient>
<!-- Icon Gradient -->
<linearGradient id="accentGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#06b6d4" />
<stop offset="50%" stop-color="#3b82f6" />
<stop offset="100%" stop-color="#6366f1" />
</linearGradient>
<!-- Glow Filter -->
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="12" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<!-- App Icon Background Rounded Rect -->
<rect width="512" height="512" rx="128" fill="url(#bgGrad)" stroke="rgba(255, 255, 255, 0.05)" stroke-width="4" />
<!-- Inner decorative tech circle -->
<circle cx="256" cy="256" r="220" fill="none" stroke="rgba(6, 182, 212, 0.1)" stroke-width="2" stroke-dasharray="10 15" />
<g filter="url(#glow)">
<!-- Speech Bubble Base -->
<path d="M 160 350
L 120 390
L 120 330
A 140 140 0 1 1 370 290
A 140 140 0 0 1 160 350 Z"
fill="none"
stroke="url(#accentGrad)"
stroke-width="16"
stroke-linejoin="round"
stroke-linecap="round" />
<!-- Intelligent Neural Nodes (Brain Paths inside Speech Bubble) -->
<!-- Center core node -->
<circle cx="256" cy="210" r="14" fill="#ffffff" />
<!-- Connections and branching nodes -->
<!-- Left top branch -->
<path d="M 256 210 L 200 170" fill="none" stroke="#06b6d4" stroke-width="8" stroke-linecap="round" />
<circle cx="200" cy="170" r="10" fill="#06b6d4" />
<!-- Right top branch -->
<path d="M 256 210 L 312 170" fill="none" stroke="#6366f1" stroke-width="8" stroke-linecap="round" />
<circle cx="312" cy="170" r="10" fill="#6366f1" />
<!-- Left bottom branch -->
<path d="M 256 210 L 190 240" fill="none" stroke="#06b6d4" stroke-width="8" stroke-linecap="round" />
<circle cx="190" cy="240" r="10" fill="#06b6d4" />
<!-- Right bottom branch -->
<path d="M 256 210 L 322 240" fill="none" stroke="#6366f1" stroke-width="8" stroke-linecap="round" />
<circle cx="322" cy="240" r="10" fill="#6366f1" />
<!-- Straight top vertical sensor -->
<path d="M 256 210 L 256 140" fill="none" stroke="#3b82f6" stroke-width="8" stroke-linecap="round" />
<circle cx="256" cy="140" r="10" fill="#3b82f6" />
<!-- Subtle horizontal link -->
<path d="M 200 170 A 70 70 0 0 1 312 170" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="4" stroke-dasharray="5 5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB