Deploy: 2026-05-23 03:23:22
This commit is contained in:
@@ -51,8 +51,19 @@ class AuthController extends BaseController
|
|||||||
'role' => 'admin'
|
'role' => 'admin'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Automatically Assign 14-Day Free Trial (Plan ID: 4)
|
||||||
|
$startsAt = date('Y-m-d H:i:s');
|
||||||
|
$endsAt = date('Y-m-d H:i:s', strtotime('+14 days'));
|
||||||
|
\App\Models\CompanySubscription::subscribeCompany(
|
||||||
|
$companyId,
|
||||||
|
4, // Trial Plan ID
|
||||||
|
14, // Duration in days
|
||||||
|
'auto_trial',
|
||||||
|
'system'
|
||||||
|
);
|
||||||
|
|
||||||
$response->json([
|
$response->json([
|
||||||
'message' => 'Company and Admin user registered successfully.',
|
'message' => 'Company and Admin user registered successfully. 14-Day Free Trial activated.',
|
||||||
'company_id' => $companyId,
|
'company_id' => $companyId,
|
||||||
'user_id' => $userId
|
'user_id' => $userId
|
||||||
], 201);
|
], 201);
|
||||||
@@ -133,15 +144,35 @@ class AuthController extends BaseController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isSuperAdmin = (int)$user['company_id'] === 1;
|
||||||
|
|
||||||
|
// Fetch subscription info for the UI
|
||||||
|
$subscription = \App\Models\CompanySubscription::findActiveByCompany($user['company_id']);
|
||||||
|
$subStatus = $subscription ? $subscription['status'] : 'expired';
|
||||||
|
$trialDaysLeft = 0;
|
||||||
|
|
||||||
|
if ($subscription && $subscription['status'] === 'active' && strpos((string)$subscription['payment_gateway'], 'auto_trial') !== false) {
|
||||||
|
$subStatus = 'trialing'; // Treat as trialing
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($subStatus === 'trialing' || ($subscription && $subscription['plan_id'] == 4)) {
|
||||||
|
$endsAt = strtotime($subscription['ends_at']);
|
||||||
|
$now = time();
|
||||||
|
$trialDaysLeft = max(0, ceil(($endsAt - $now) / 86400));
|
||||||
|
}
|
||||||
|
|
||||||
$response->json([
|
$response->json([
|
||||||
'user' => [
|
'user' => [
|
||||||
'id' => $user['id'],
|
'id' => $user['id'],
|
||||||
'company_id' => $user['company_id'],
|
'company_id' => $user['company_id'],
|
||||||
'name' => $user['name'],
|
'name' => $user['name'],
|
||||||
'email' => Security::decrypt($user['email']), // Decrypt email before sending back
|
'email' => Security::decrypt($user['email']),
|
||||||
'role' => $user['role'],
|
'role' => $user['role'],
|
||||||
'status' => $user['status'],
|
'status' => $user['status'],
|
||||||
'created_at' => $user['created_at']
|
'created_at' => $user['created_at'],
|
||||||
|
'is_super_admin' => $isSuperAdmin,
|
||||||
|
'subscription_status' => $subStatus,
|
||||||
|
'trial_days_left' => $trialDaysLeft
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
86
backend/app/Controllers/BillingController.php
Normal file
86
backend/app/Controllers/BillingController.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Core\Request;
|
||||||
|
use App\Core\Response;
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
class BillingController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all available subscription plans
|
||||||
|
* GET /api/plans
|
||||||
|
*/
|
||||||
|
public function getPlans(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
// Don't expose the Trial plan (ID 4) as an upgrade option, only paid ones.
|
||||||
|
$plans = Database::select("SELECT * FROM subscription_plans WHERE price > 0 ORDER BY price ASC");
|
||||||
|
|
||||||
|
$response->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => $plans
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade or submit payment for a plan
|
||||||
|
* POST /api/billing/upgrade
|
||||||
|
*/
|
||||||
|
public function upgrade(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
$companyId = $request->company_id;
|
||||||
|
$body = $request->getBody();
|
||||||
|
|
||||||
|
$planId = $body['plan_id'] ?? null;
|
||||||
|
$paymentMethod = $body['payment_method'] ?? 'manual'; // 'paymob', 'cliq', 'binance', etc.
|
||||||
|
$receiptReference = $body['receipt_reference'] ?? null;
|
||||||
|
|
||||||
|
if (!$planId) {
|
||||||
|
$response->status(400)->json(['error' => 'Missing plan_id']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$plan = Database::selectOne("SELECT * FROM subscription_plans WHERE id = ?", [$planId]);
|
||||||
|
if (!$plan) {
|
||||||
|
$response->status(404)->json(['error' => 'Plan not found']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($paymentMethod === 'paymob') {
|
||||||
|
// Here we would integrate Paymob API to generate a payment link
|
||||||
|
// For now, we simulate returning a checkout URL.
|
||||||
|
$checkoutUrl = "https://paymob.com/checkout/mock_url_for_plan_{$planId}_company_{$companyId}";
|
||||||
|
|
||||||
|
$response->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Redirect to Paymob to complete payment',
|
||||||
|
'checkout_url' => $checkoutUrl
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Manual Payment (CliQ, Binance, Bank Transfer)
|
||||||
|
if (empty($receiptReference)) {
|
||||||
|
$response->status(400)->json(['error' => 'Please provide a receipt reference or transaction ID']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete any existing pending requests for this company to avoid spam
|
||||||
|
Database::execute("DELETE FROM company_subscriptions WHERE company_id = ? AND status = 'pending_approval'", [$companyId]);
|
||||||
|
|
||||||
|
// Insert a pending subscription request
|
||||||
|
Database::execute("
|
||||||
|
INSERT INTO company_subscriptions (company_id, plan_id, status, starts_at, ends_at, payment_method, receipt_reference)
|
||||||
|
VALUES (?, ?, 'pending_approval', NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), ?, ?)
|
||||||
|
", [$companyId, $planId, $paymentMethod, $receiptReference]);
|
||||||
|
|
||||||
|
$response->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Payment receipt submitted successfully. Your account will be upgraded after admin approval.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->status(500)->json(['error' => 'Failed to process upgrade request: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,24 +46,28 @@ class SuperAdminController extends BaseController
|
|||||||
cs.plan_id,
|
cs.plan_id,
|
||||||
sp.name as plan_name,
|
sp.name as plan_name,
|
||||||
cs.status as subscription_status,
|
cs.status as subscription_status,
|
||||||
cs.starts_at as subscription_starts,
|
SELECT c.id, c.name, c.status as company_status, c.created_at,
|
||||||
cs.ends_at as subscription_ends,
|
cs.plan_id, cs.status as sub_status, cs.starts_at, cs.ends_at, cs.payment_method, cs.receipt_reference,
|
||||||
(SELECT COUNT(*) FROM whatsapp_sessions WHERE company_id = c.id) as sessions_count,
|
p.name as plan_name
|
||||||
(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
|
FROM companies c
|
||||||
LEFT JOIN company_subscriptions cs ON cs.company_id = c.id AND cs.status = 'active'
|
LEFT JOIN company_subscriptions cs ON c.id = cs.company_id AND cs.status IN ('active', 'trialing', 'pending_approval')
|
||||||
LEFT JOIN subscription_plans sp ON cs.plan_id = sp.id
|
LEFT JOIN subscription_plans p ON cs.plan_id = p.id
|
||||||
LEFT JOIN company_subscription_usage cu ON cu.company_id = c.id
|
ORDER BY c.created_at DESC
|
||||||
AND cu.billing_start <= CURRENT_DATE()
|
|
||||||
AND cu.billing_end >= CURRENT_DATE()
|
|
||||||
ORDER BY c.id ASC
|
|
||||||
");
|
");
|
||||||
|
|
||||||
// Fetch list of available subscription plans
|
$pendingApprovals = [];
|
||||||
$plans = Database::select("SELECT id, name, price, max_sessions FROM subscription_plans ORDER BY price ASC");
|
$activeCompanies = [];
|
||||||
|
|
||||||
|
foreach ($companies as $c) {
|
||||||
|
if ($c['sub_status'] === 'pending_approval') {
|
||||||
|
$pendingApprovals[] = $c;
|
||||||
|
} else {
|
||||||
|
$activeCompanies[] = $c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fetch all available plans to allow admin to upgrade them manually
|
||||||
|
$plans = Database::select("SELECT id, name, price FROM subscription_plans ORDER BY price ASC");
|
||||||
|
|
||||||
$response->json([
|
$response->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
@@ -73,7 +77,8 @@ class SuperAdminController extends BaseController
|
|||||||
'total_sessions' => (int)$sessionsCount,
|
'total_sessions' => (int)$sessionsCount,
|
||||||
'connected_sessions' => (int)$connectedSessions
|
'connected_sessions' => (int)$connectedSessions
|
||||||
],
|
],
|
||||||
'companies' => $companies,
|
'companies' => $activeCompanies,
|
||||||
|
'pending_approvals' => $pendingApprovals,
|
||||||
'plans' => $plans
|
'plans' => $plans
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@@ -138,4 +143,46 @@ class SuperAdminController extends BaseController
|
|||||||
$response->status(500)->json(['error' => 'Failed to update subscription: ' . $e->getMessage()]);
|
$response->status(500)->json(['error' => 'Failed to update subscription: ' . $e->getMessage()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approve a pending billing request
|
||||||
|
* POST /api/admin/companies/approve-billing
|
||||||
|
*/
|
||||||
|
public function approveBilling(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
$body = $request->getBody();
|
||||||
|
$targetCompanyId = isset($body['company_id']) ? (int)$body['company_id'] : null;
|
||||||
|
|
||||||
|
if (!$targetCompanyId) {
|
||||||
|
$response->status(400)->json(['error' => 'Missing company_id']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the pending subscription
|
||||||
|
$pending = Database::selectOne("SELECT * FROM company_subscriptions WHERE company_id = ? AND status = 'pending_approval' ORDER BY id DESC LIMIT 1", [$targetCompanyId]);
|
||||||
|
if (!$pending) {
|
||||||
|
$response->status(404)->json(['error' => 'No pending approval found for this company']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate other active/trialing subscriptions
|
||||||
|
Database::execute("UPDATE company_subscriptions SET status = 'expired' WHERE company_id = ? AND status IN ('active', 'trialing')", [$targetCompanyId]);
|
||||||
|
|
||||||
|
// Mark the pending one as active
|
||||||
|
Database::execute("UPDATE company_subscriptions SET status = 'active' WHERE id = ?", [$pending['id']]);
|
||||||
|
|
||||||
|
// Clear active subscription cache for the company
|
||||||
|
if (class_exists('App\Core\Cache')) {
|
||||||
|
\App\Core\Cache::delete("company_subscription_{$targetCompanyId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Billing approved and subscription activated successfully'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->status(500)->json(['error' => 'Failed to approve billing: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,7 +442,27 @@ class WhatsAppController extends BaseController
|
|||||||
} elseif ($body['state'] === 'connected') {
|
} elseif ($body['state'] === 'connected') {
|
||||||
$updateData['qr_code'] = null; // Clear QR when connected
|
$updateData['qr_code'] = null; // Clear QR when connected
|
||||||
if (!empty($body['phone'])) {
|
if (!empty($body['phone'])) {
|
||||||
$updateData['phone'] = $body['phone'];
|
// Anti-Abuse: Prevent Duplicate Phone Numbers Across Companies
|
||||||
|
$existingPhoneSession = \App\Models\WhatsAppSession::findByPhone($body['phone']);
|
||||||
|
if ($existingPhoneSession && (int)$existingPhoneSession['company_id'] !== (int)$session['company_id']) {
|
||||||
|
// This phone is already linked to another company! We must disconnect this malicious session.
|
||||||
|
$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']);
|
||||||
|
curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
error_log("Anti-Abuse Block: Company ID {$session['company_id']} tried to link phone {$body['phone']} which is already linked to Company ID {$existingPhoneSession['company_id']}. Session disconnected.");
|
||||||
|
|
||||||
|
$updateData['status'] = 'disconnected';
|
||||||
|
$updateData['qr_code'] = null;
|
||||||
|
} else {
|
||||||
|
$updateData['phone'] = $body['phone'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} elseif ($body['state'] === 'disconnected') {
|
} elseif ($body['state'] === 'disconnected') {
|
||||||
$updateData['qr_code'] = null;
|
$updateData['qr_code'] = null;
|
||||||
|
|||||||
@@ -86,6 +86,25 @@ class WhatsAppSession extends BaseModel
|
|||||||
return $session;
|
return $session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find session by phone number (useful to prevent duplicates across companies)
|
||||||
|
*/
|
||||||
|
public static function findByPhone(string $phone)
|
||||||
|
{
|
||||||
|
$phoneHash = Security::blindIndex($phone);
|
||||||
|
$session = Database::selectOne(
|
||||||
|
"SELECT * FROM " . static::$table . " WHERE phone_hash = ? LIMIT 1",
|
||||||
|
[$phoneHash]
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create or retrieve a new session for a company
|
* Create or retrieve a new session for a company
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ CREATE TABLE IF NOT EXISTS `company_subscriptions` (
|
|||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
`company_id` INT NOT NULL,
|
`company_id` INT NOT NULL,
|
||||||
`plan_id` INT NOT NULL,
|
`plan_id` INT NOT NULL,
|
||||||
`status` ENUM('active', 'trialing', 'canceled', 'expired', 'past_due') DEFAULT 'active',
|
`status` ENUM('active', 'trialing', 'canceled', 'expired', 'past_due', 'pending_approval') DEFAULT 'active',
|
||||||
`starts_at` TIMESTAMP NOT NULL,
|
`starts_at` TIMESTAMP NOT NULL,
|
||||||
`ends_at` TIMESTAMP NOT NULL,
|
`ends_at` TIMESTAMP NOT NULL,
|
||||||
`canceled_at` TIMESTAMP NULL DEFAULT NULL,
|
`canceled_at` TIMESTAMP NULL DEFAULT NULL,
|
||||||
`payment_gateway` VARCHAR(50) NULL,
|
`payment_gateway` VARCHAR(50) NULL,
|
||||||
|
`payment_method` VARCHAR(50) NULL COMMENT 'paymob, cliq, binance',
|
||||||
|
`receipt_reference` VARCHAR(255) NULL COMMENT 'External transaction ID or receipt URL',
|
||||||
`subscription_ref` VARCHAR(255) NULL COMMENT 'External subscription ID reference',
|
`subscription_ref` VARCHAR(255) NULL COMMENT 'External subscription ID reference',
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|||||||
39
backend/migrate_billing_tables.php
Normal file
39
backend/migrate_billing_tables.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
if (php_sapi_name() !== 'cli') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Access denied.');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/app/bootstrap.php';
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
try {
|
||||||
|
echo "=== Migrating Billing Tables ===\n";
|
||||||
|
|
||||||
|
// 1. Alter company_subscriptions to support new fields and ENUM
|
||||||
|
Database::execute("
|
||||||
|
ALTER TABLE company_subscriptions
|
||||||
|
MODIFY COLUMN status ENUM('active', 'trialing', 'canceled', 'expired', 'past_due', 'pending_approval') DEFAULT 'active';
|
||||||
|
");
|
||||||
|
echo "✅ Updated 'status' ENUM in company_subscriptions.\n";
|
||||||
|
|
||||||
|
// Add payment_method if it doesn't exist
|
||||||
|
try {
|
||||||
|
Database::execute("ALTER TABLE company_subscriptions ADD COLUMN payment_method VARCHAR(50) NULL COMMENT 'paymob, cliq, binance' AFTER payment_gateway");
|
||||||
|
echo "✅ Added 'payment_method' column.\n";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "ℹ️ Column 'payment_method' may already exist. Skipping.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add receipt_reference if it doesn't exist
|
||||||
|
try {
|
||||||
|
Database::execute("ALTER TABLE company_subscriptions ADD COLUMN receipt_reference VARCHAR(255) NULL COMMENT 'External transaction ID or receipt URL' AFTER payment_method");
|
||||||
|
echo "✅ Added 'receipt_reference' column.\n";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "ℹ️ Column 'receipt_reference' may already exist. Skipping.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Migration Completed Successfully! ===\n";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "❌ Migration Failed: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
@@ -760,7 +760,7 @@
|
|||||||
<!-- Left Sidebar Nav -->
|
<!-- Left Sidebar Nav -->
|
||||||
<div class="nav-menu">
|
<div class="nav-menu">
|
||||||
<!-- Super Admin Dashboard -->
|
<!-- Super Admin Dashboard -->
|
||||||
<button class="nav-item" x-show="user?.is_super_admin" :class="{ 'active': activeDashboardTab === 'super_admin' }" @click="activeDashboardTab = 'super_admin'; fetchSuperAdminStats()" id="nav-superadmin-btn">
|
<button class="nav-item" x-show="user?.is_super_admin" :class="{ 'active': activeDashboardTab === 'super_admin' }" @click="activeDashboardTab = 'super_admin'; fetchSuperAdminStats()" id="nav-superadmin-btn">
|
||||||
<span class="nav-icon">👑</span>
|
<span class="nav-icon">👑</span>
|
||||||
<span class="nav-text" x-text="lang === 'ar' ? 'لوحة المشرف العام' : 'Super Admin'"></span>
|
<span class="nav-text" x-text="lang === 'ar' ? 'لوحة المشرف العام' : 'Super Admin'"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -768,6 +768,9 @@
|
|||||||
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'whatsapp' }" @click="activeDashboardTab = 'whatsapp'" id="nav-whatsapp-btn">
|
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'whatsapp' }" @click="activeDashboardTab = 'whatsapp'" id="nav-whatsapp-btn">
|
||||||
<span>📱</span> <span x-text="lang === 'ar' ? 'اتصال الواتساب' : 'WhatsApp Connection'"></span>
|
<span>📱</span> <span x-text="lang === 'ar' ? 'اتصال الواتساب' : 'WhatsApp Connection'"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'billing' }" @click="activeDashboardTab = 'billing'; fetchPlans()" id="nav-billing-btn">
|
||||||
|
<span>💳</span> <span x-text="lang === 'ar' ? 'الباقات والاشتراكات' : 'Billing & Plans'"></span>
|
||||||
|
</button>
|
||||||
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'contacts' }" @click="activeDashboardTab = 'contacts'; fetchContacts(); fetchGroups()" id="nav-contacts-btn">
|
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'contacts' }" @click="activeDashboardTab = 'contacts'; fetchContacts(); fetchGroups()" id="nav-contacts-btn">
|
||||||
<span>👥</span> <span x-text="lang === 'ar' ? 'دليل جهات الاتصال' : 'Contacts Directory'"></span>
|
<span>👥</span> <span x-text="lang === 'ar' ? 'دليل جهات الاتصال' : 'Contacts Directory'"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -790,6 +793,11 @@
|
|||||||
|
|
||||||
<!-- Right Dashboard Panels -->
|
<!-- Right Dashboard Panels -->
|
||||||
<div style="flex: 1;">
|
<div style="flex: 1;">
|
||||||
|
<template x-if="user?.subscription_status === 'trialing' || (user?.subscription_status === 'active' && user?.trial_days_left < 14)">
|
||||||
|
<div class="banner banner-warning" style="margin-bottom: 1.5rem;">
|
||||||
|
<span x-text="lang === 'ar' ? 'متبقي لك ' + user.trial_days_left + ' أيام من الفترة التجريبية. اشترك الآن لضمان استمرار الخدمة.' : 'You have ' + user.trial_days_left + ' days left in your trial. Subscribe now to ensure service continuity.'"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<!-- Global Dashboard Banner -->
|
<!-- Global Dashboard Banner -->
|
||||||
<template x-if="dashboardSuccess">
|
<template x-if="dashboardSuccess">
|
||||||
<div class="banner banner-success" style="margin-bottom: 1.5rem;" id="dashboard-success-banner">
|
<div class="banner banner-success" style="margin-bottom: 1.5rem;" id="dashboard-success-banner">
|
||||||
@@ -821,6 +829,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 2rem; margin-bottom: 2rem;" x-show="superAdminPending?.length > 0">
|
||||||
|
<h3 style="margin-bottom: 1rem;"><span style="color: var(--warning-color);">⏳</span> <span x-text="lang === 'ar' ? 'طلبات الدفع بانتظار الموافقة' : 'Pending Approvals'"></span></h3>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th x-text="lang === 'ar' ? 'معرف الشركة' : 'Company ID'"></th>
|
||||||
|
<th x-text="lang === 'ar' ? 'اسم الشركة' : 'Company Name'"></th>
|
||||||
|
<th x-text="lang === 'ar' ? 'الباقة المطلوبة' : 'Requested Plan'"></th>
|
||||||
|
<th x-text="lang === 'ar' ? 'طريقة الدفع' : 'Payment Method'"></th>
|
||||||
|
<th x-text="lang === 'ar' ? 'رقم الحوالة' : 'Receipt Reference'"></th>
|
||||||
|
<th x-text="lang === 'ar' ? 'الإجراء' : 'Action'"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="req in superAdminPending" :key="req.company_id">
|
||||||
|
<tr>
|
||||||
|
<td x-text="'#' + req.id"></td>
|
||||||
|
<td x-text="req.name"></td>
|
||||||
|
<td><span class="badge badge-info" x-text="req.plan_name"></span></td>
|
||||||
|
<td x-text="req.payment_method?.toUpperCase()"></td>
|
||||||
|
<td x-text="req.receipt_reference"></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary btn-sm" @click="approveBilling(req.id)" x-text="lang === 'ar' ? 'موافقة وتفعيل' : 'Approve & Activate'"></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 style="margin-top:2rem;" x-text="lang === 'ar' ? 'قائمة الشركات' : 'Companies List'"></h3>
|
<h3 style="margin-top:2rem;" x-text="lang === 'ar' ? 'قائمة الشركات' : 'Companies List'"></h3>
|
||||||
<div class="data-table" style="overflow-x: auto;">
|
<div class="data-table" style="overflow-x: auto;">
|
||||||
<table>
|
<table>
|
||||||
@@ -1020,6 +1060,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel: Billing & Plans -->
|
||||||
|
<div class="panel" x-show="activeDashboardTab === 'billing'" id="panel-billing">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
|
||||||
|
<h2 style="font-size: 1.4rem; margin: 0;" x-text="lang === 'ar' ? 'الباقات والاشتراكات' : 'Billing & Plans'"></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem;" :style="lang === 'ar' ? 'direction: rtl;' : ''">
|
||||||
|
<template x-for="plan in availablePlans" :key="plan.id">
|
||||||
|
<div class="card" style="border: 2px solid transparent; transition: 0.3s; padding: 2rem; text-align: center;" :style="userPlanId == plan.id ? 'border-color: var(--primary-accent);' : ''">
|
||||||
|
<h3 style="font-size: 1.5rem; margin-bottom: 0.5rem;" x-text="plan.name"></h3>
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-accent); margin-bottom: 1rem;">
|
||||||
|
$<span x-text="plan.price"></span> <span style="font-size: 1rem; color: var(--text-muted);">/ <span x-text="lang === 'ar' ? 'شهر' : 'mo'"></span></span>
|
||||||
|
</div>
|
||||||
|
<ul style="list-style: none; padding: 0; margin-bottom: 1.5rem; text-align: start; line-height: 1.8;">
|
||||||
|
<li><span style="margin-right: 0.5rem;">✅</span> <span x-text="plan.max_sessions"></span> <span x-text="lang === 'ar' ? 'أرقام واتساب' : 'WhatsApp Numbers'"></span></li>
|
||||||
|
<li><span style="margin-right: 0.5rem;">✅</span> <span x-text="plan.max_requests"></span> <span x-text="lang === 'ar' ? 'رسالة نصية' : 'Text Messages'"></span></li>
|
||||||
|
<li><span style="margin-right: 0.5rem;">✅</span> <span x-text="plan.max_voice_requests"></span> <span x-text="lang === 'ar' ? 'رسالة صوتية' : 'Voice Notes'"></span></li>
|
||||||
|
</ul>
|
||||||
|
<button class="btn btn-primary" style="width: 100%;" @click="openCheckoutModal(plan)" :disabled="userPlanId == plan.id" x-text="userPlanId == plan.id ? (lang === 'ar' ? 'باقتك الحالية' : 'Current Plan') : (lang === 'ar' ? 'ترقية الآن' : 'Upgrade Now')"></button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Panel: Contacts Directory -->
|
<!-- Panel: Contacts Directory -->
|
||||||
<div class="panel" x-show="activeDashboardTab === 'contacts'" id="panel-contacts">
|
<div class="panel" x-show="activeDashboardTab === 'contacts'" id="panel-contacts">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
|
||||||
@@ -1216,7 +1280,7 @@
|
|||||||
💡 نصائح وتوجيهات لكتابة تعليمات ممتازة:
|
💡 نصائح وتوجيهات لكتابة تعليمات ممتازة:
|
||||||
</strong>
|
</strong>
|
||||||
<ul style="list-style-type: disc; margin-right: 1.25rem; padding-left: 0; line-height: 1.5; margin-bottom: 0;">
|
<ul style="list-style-type: disc; margin-right: 1.25rem; padding-left: 0; line-height: 1.5; margin-bottom: 0;">
|
||||||
<li><strong>الهوية والاسم:</strong> حدد اسم الروبوت بوضوح (مثال: "أنا سارة من فريق تطبيق نبيه").</li>
|
<li><strong>الهوية والاسم:</strong> حدد اسم الروبوت بوحضوح (مثال: "أنا سارة من فريق تطبيق نبيه").</li>
|
||||||
<li><strong>اللهجة والأسلوب:</strong> اطلب من الذكاء الاصطناعي الرد بلهجة معينة (مثال: اللهجة السورية أو الفصحى المبسطة).</li>
|
<li><strong>اللهجة والأسلوب:</strong> اطلب من الذكاء الاصطناعي الرد بلهجة معينة (مثال: اللهجة السورية أو الفصحى المبسطة).</li>
|
||||||
<li><strong>البيانات الأساسية:</strong> اكتب ساعات عمل المتجر، طرق الدفع والتوصيل، وسياسة الاستبدال لكي يجيب الروبوت بدقة.</li>
|
<li><strong>البيانات الأساسية:</strong> اكتب ساعات عمل المتجر، طرق الدفع والتوصيل، وسياسة الاستبدال لكي يجيب الروبوت بدقة.</li>
|
||||||
<li><strong>التعليمات اللغوية:</strong> قمنا بتضمين ميزة مطابقة اللغة تلقائياً (الرد بالإنجليزية على الرسائل الإنجليزية، وبالعربية على العربية).</li>
|
<li><strong>التعليمات اللغوية:</strong> قمنا بتضمين ميزة مطابقة اللغة تلقائياً (الرد بالإنجليزية على الرسائل الإنجليزية، وبالعربية على العربية).</li>
|
||||||
@@ -1455,6 +1519,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Checkout -->
|
||||||
|
<div class="modal-overlay" x-show="showCheckoutModal" id="modal-checkout" style="display: none;">
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title" x-text="lang === 'ar' ? 'إتمام عملية الدفع' : 'Complete Checkout'"></h3>
|
||||||
|
<button class="modal-close" @click="showCheckoutModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p x-text="lang === 'ar' ? 'أنت تقوم بترقية حسابك إلى باقة: ' + selectedCheckoutPlan?.name : 'You are upgrading to: ' + selectedCheckoutPlan?.name"></p>
|
||||||
|
<p style="font-weight: bold; font-size: 1.2rem; margin-bottom: 1rem;">
|
||||||
|
<span x-text="lang === 'ar' ? 'المبلغ المطلوب: ' : 'Total Amount: '"></span> $<span x-text="selectedCheckoutPlan?.price"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" x-text="lang === 'ar' ? 'طريقة الدفع' : 'Payment Method'"></label>
|
||||||
|
<select x-model="checkoutPaymentMethod" class="form-input">
|
||||||
|
<option value="paymob" x-text="lang === 'ar' ? 'البطاقة الائتمانية (Paymob)' : 'Credit Card (Paymob)'"></option>
|
||||||
|
<option value="cliq" x-text="lang === 'ar' ? 'كليك (CliQ) - الأردن' : 'CliQ Transfer (Jordan)'"></option>
|
||||||
|
<option value="binance" x-text="lang === 'ar' ? 'بينانس (Binance Pay)' : 'Crypto (Binance)'"></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="checkoutPaymentMethod !== 'paymob'">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" x-text="lang === 'ar' ? 'رقم الحوالة / Transaction ID' : 'Receipt Reference / TXID'"></label>
|
||||||
|
<input type="text" x-model="checkoutReceipt" class="form-input" :placeholder="lang === 'ar' ? 'أدخل رقم الحوالة هنا' : 'Enter reference here'">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" @click="showCheckoutModal = false" x-text="lang === 'ar' ? 'إلغاء' : 'Cancel'"></button>
|
||||||
|
<button class="btn btn-primary" @click="submitCheckout()" :disabled="actionLoading">
|
||||||
|
<span x-show="!actionLoading" x-text="lang === 'ar' ? 'تأكيد الدفع' : 'Confirm Payment'"></span>
|
||||||
|
<span x-show="actionLoading" class="spinner"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Modal: Add Contact -->
|
<!-- Modal: Add Contact -->
|
||||||
<div class="modal-overlay" x-show="showAddContactModal" id="modal-add-contact" style="display: none;">
|
<div class="modal-overlay" x-show="showAddContactModal" id="modal-add-contact" style="display: none;">
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
@@ -1711,6 +1814,7 @@
|
|||||||
// Dashboard States
|
// Dashboard States
|
||||||
superAdminStats: null,
|
superAdminStats: null,
|
||||||
superAdminCompanies: [],
|
superAdminCompanies: [],
|
||||||
|
superAdminPending: [],
|
||||||
superAdminPlans: [],
|
superAdminPlans: [],
|
||||||
activeDashboardTab: 'whatsapp',
|
activeDashboardTab: 'whatsapp',
|
||||||
whatsappSession: null,
|
whatsappSession: null,
|
||||||
@@ -1725,6 +1829,16 @@
|
|||||||
password: '',
|
password: '',
|
||||||
whatsapp_session_id: ''
|
whatsapp_session_id: ''
|
||||||
},
|
},
|
||||||
|
// Billing State
|
||||||
|
availablePlans: [],
|
||||||
|
userPlanId: null,
|
||||||
|
showCheckoutModal: false,
|
||||||
|
selectedCheckoutPlan: null,
|
||||||
|
checkoutPaymentMethod: 'paymob',
|
||||||
|
checkoutReceipt: '',
|
||||||
|
checkoutError: '',
|
||||||
|
checkoutSuccess: '',
|
||||||
|
|
||||||
woocommerceStatus: null,
|
woocommerceStatus: null,
|
||||||
woocommerceLoading: false,
|
woocommerceLoading: false,
|
||||||
wooForm: {
|
wooForm: {
|
||||||
@@ -1934,6 +2048,71 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Billing & Checkout Methods
|
||||||
|
async fetchPlans() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/plans', {
|
||||||
|
headers: { 'Authorization': `Bearer ${this.token}` }
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok && data.status === 'success') {
|
||||||
|
this.availablePlans = data.data;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch plans', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openCheckoutModal(plan) {
|
||||||
|
this.selectedCheckoutPlan = plan;
|
||||||
|
this.checkoutPaymentMethod = 'paymob';
|
||||||
|
this.checkoutReceipt = '';
|
||||||
|
this.checkoutError = '';
|
||||||
|
this.checkoutSuccess = '';
|
||||||
|
this.showCheckoutModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitCheckout() {
|
||||||
|
if (this.checkoutPaymentMethod !== 'paymob' && !this.checkoutReceipt) {
|
||||||
|
this.checkoutError = this.lang === 'ar' ? 'الرجاء إدخال رقم الحوالة' : 'Please enter receipt reference';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actionLoading = true;
|
||||||
|
this.checkoutError = '';
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/billing/upgrade', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
plan_id: this.selectedCheckoutPlan.id,
|
||||||
|
payment_method: this.checkoutPaymentMethod,
|
||||||
|
receipt_reference: this.checkoutReceipt
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok && data.status === 'success') {
|
||||||
|
if (this.checkoutPaymentMethod === 'paymob' && data.checkout_url) {
|
||||||
|
window.location.href = data.checkout_url;
|
||||||
|
} else {
|
||||||
|
this.checkoutSuccess = data.message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showCheckoutModal = false;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.checkoutError = data.error || 'Failed to submit payment request';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.checkoutError = 'Network error while submitting payment';
|
||||||
|
} finally {
|
||||||
|
this.actionLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async fetchWhatsappStatus() {
|
async fetchWhatsappStatus() {
|
||||||
if (!this.token) return;
|
if (!this.token) return;
|
||||||
const queryParam = this.whatsappSession ? `?session_id=${this.whatsappSession.id}` : '';
|
const queryParam = this.whatsappSession ? `?session_id=${this.whatsappSession.id}` : '';
|
||||||
@@ -1971,6 +2150,7 @@
|
|||||||
if (response.ok && data.status === 'success') {
|
if (response.ok && data.status === 'success') {
|
||||||
this.superAdminStats = data.data.stats;
|
this.superAdminStats = data.data.stats;
|
||||||
this.superAdminCompanies = data.data.companies;
|
this.superAdminCompanies = data.data.companies;
|
||||||
|
this.superAdminPending = data.data.pending_approvals;
|
||||||
this.superAdminPlans = data.data.plans;
|
this.superAdminPlans = data.data.plans;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ $router->post('/api/otp/send', [\App\Controllers\OTPController::class
|
|||||||
// Super Admin Routes
|
// Super Admin Routes
|
||||||
$router->get('/api/admin/stats', [\App\Controllers\SuperAdminController::class, 'getStats'], [\App\Middlewares\AuthMiddleware::class]);
|
$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]);
|
$router->post('/api/admin/companies/subscribe', [\App\Controllers\SuperAdminController::class, 'subscribeCompany'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
|
$router->post('/api/admin/companies/approve-billing', [\App\Controllers\SuperAdminController::class, 'approveBilling'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
|
|
||||||
|
// Billing & Subscription Routes
|
||||||
|
$router->get('/api/plans', [\App\Controllers\BillingController::class, 'getPlans'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
|
$router->post('/api/billing/upgrade', [\App\Controllers\BillingController::class, 'upgrade'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
|
|
||||||
// Phase 4 & 5: CRM, Templates & Campaigns Routes
|
// Phase 4 & 5: CRM, Templates & Campaigns Routes
|
||||||
$router->get('/api/contacts', [\App\Controllers\ContactController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
|
$router->get('/api/contacts', [\App\Controllers\ContactController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
|
|||||||
@@ -42,7 +42,17 @@ try {
|
|||||||
'max_requests' => 15000,
|
'max_requests' => 15000,
|
||||||
'max_voice_requests' => 2000,
|
'max_voice_requests' => 2000,
|
||||||
'max_ocr_requests' => 2000,
|
'max_ocr_requests' => 2000,
|
||||||
'features' => json_encode(['text_chatbot' => true, 'campaigns' => true, 'woocommerce' => true, 'salla' => true, 'webhooks' => true])
|
'features' => json_encode(['text_chatbot' => true, 'campaigns' => true, 'woocommerce' => true, 'salla' => true, 'webhooks' => true, 'voice' => true])
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 4,
|
||||||
|
'name' => 'Trial (14 Days)',
|
||||||
|
'price' => 0.00,
|
||||||
|
'max_sessions' => 1,
|
||||||
|
'max_requests' => 5000,
|
||||||
|
'max_voice_requests' => 100,
|
||||||
|
'max_ocr_requests' => 100,
|
||||||
|
'features' => json_encode(['text_chatbot' => true, 'campaigns' => true, 'woocommerce' => true, 'salla' => true, 'webhooks' => true, 'voice' => true])
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user