Files
nabeh/backend/public/admin.html
2026-05-23 01:13:51 +03:00

1028 lines
40 KiB
HTML

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>نابه | لوحة التحكم للمشرف العام</title>
<!-- Premium Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700;800&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Alpine.js CDN -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
:root {
--bg-app: #030712;
--bg-glow-1: rgba(99, 102, 241, 0.12); /* Indigo glow */
--bg-glow-2: rgba(14, 165, 233, 0.08); /* Cyan glow */
--card-bg: rgba(17, 24, 39, 0.55);
--card-border: rgba(255, 255, 255, 0.06);
--card-border-hover: rgba(255, 255, 255, 0.15);
--primary: #6366f1;
--primary-hover: #4f46e5;
--secondary: #0ea5e9;
--text-main: #f3f4f6;
--text-muted: #9ca3af;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--font-ar: 'Cairo', sans-serif;
--font-en: 'Outfit', sans-serif;
--glass-blur: blur(20px);
}
[dir="ltr"] {
--font-ar: 'Outfit', sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
background-color: var(--bg-app);
font-family: var(--font-ar);
color: var(--text-main);
min-height: 100vh;
overflow-x: hidden;
position: relative;
}
/* Ambient Animated Background Glows */
.ambient-glows {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
pointer-events: none;
overflow: hidden;
}
.glow-1 {
position: absolute;
top: -10%;
right: 15%;
width: 50vw;
height: 50vw;
background: radial-gradient(circle, var(--bg-glow-1) 0%, transparent 70%);
filter: blur(80px);
animation: floatGlow 12s infinite alternate;
}
.glow-2 {
position: absolute;
bottom: -5%;
left: 10%;
width: 45vw;
height: 45vw;
background: radial-gradient(circle, var(--bg-glow-2) 0%, transparent 75%);
filter: blur(80px);
animation: floatGlow 18s infinite alternate-reverse;
}
@keyframes floatGlow {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(4%, 5%) scale(1.15); }
}
/* Navigation Header */
header {
position: sticky;
top: 0;
width: 100%;
background: rgba(3, 7, 18, 0.6);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-bottom: 1px solid var(--card-border);
z-index: 100;
padding: 1rem 2rem;
}
.nav-container {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo-section {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
color: inherit;
}
.logo-section img {
height: 36px;
width: auto;
}
.logo-text {
font-size: 1.5rem;
font-weight: 800;
background: linear-gradient(135deg, #f3f4f6 0%, #a5b4fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-actions {
display: flex;
align-items: center;
gap: 1rem;
}
/* Glassmorphism Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.6rem 1.2rem;
border-radius: 12px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
border: 1px solid transparent;
text-decoration: none;
font-family: inherit;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
color: #ffffff;
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.35);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(99, 102, 241, 0.5);
}
.btn-glass {
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--card-border);
color: var(--text-main);
}
.btn-glass:hover {
background: rgba(255, 255, 255, 0.08);
border-color: var(--card-border-hover);
}
.btn-danger {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
.btn-danger:hover {
background: var(--danger);
color: #ffffff;
}
/* Container & Grid Layouts */
.main-container {
max-width: 1400px;
margin: 2rem auto;
padding: 0 1.5rem;
}
/* Stats Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.stat-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 18px;
padding: 1.75rem;
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 80% 20%, rgba(99, 102, 241, 0.06) 0%, transparent 50%);
pointer-events: none;
}
.stat-card:hover {
border-color: var(--card-border-hover);
transform: translateY(-3px);
}
.stat-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.stat-title {
font-size: 0.95rem;
color: var(--text-muted);
font-weight: 500;
}
.stat-value {
font-size: 2.2rem;
font-weight: 800;
font-family: var(--font-en);
background: linear-gradient(135deg, #ffffff 0%, #cbd5e1 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-icon {
width: 52px;
height: 52px;
border-radius: 14px;
background: rgba(99, 102, 241, 0.1);
color: var(--primary);
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(99, 102, 241, 0.2);
}
/* Glassmorphic Table Card */
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 20px;
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.card-title {
font-size: 1.25rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Search input styling */
.search-box {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 0.5rem 1rem;
max-width: 320px;
width: 100%;
}
.search-box:focus-within {
border-color: var(--primary);
background: rgba(255, 255, 255, 0.05);
}
.search-box input {
background: transparent;
border: none;
outline: none;
color: var(--text-main);
width: 100%;
font-family: inherit;
font-size: 0.9rem;
}
/* Table Design */
.table-responsive {
width: 100%;
overflow-x: auto;
border-radius: 12px;
}
table {
width: 100%;
border-collapse: collapse;
text-align: right;
}
[dir="ltr"] table {
text-align: left;
}
th {
background: rgba(255, 255, 255, 0.02);
padding: 1rem 1.25rem;
color: var(--text-muted);
font-weight: 600;
font-size: 0.85rem;
border-bottom: 1px solid var(--card-border);
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
font-size: 0.9rem;
vertical-align: middle;
}
tr:hover td {
background: rgba(255, 255, 255, 0.015);
}
/* Badges */
.badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.65rem;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 700;
}
.badge-success {
background: rgba(16, 185, 129, 0.12);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.25);
}
.badge-warning {
background: rgba(245, 158, 11, 0.12);
color: var(--warning);
border: 1px solid rgba(245, 158, 11, 0.25);
}
.badge-danger {
background: rgba(239, 68, 68, 0.12);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.25);
}
.badge-primary {
background: rgba(99, 102, 241, 0.12);
color: #a5b4fc;
border: 1px solid rgba(99, 102, 241, 0.25);
}
/* Usage bars */
.usage-bar-container {
width: 100%;
max-width: 150px;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.usage-bar-track {
height: 6px;
background: rgba(255, 255, 255, 0.06);
border-radius: 3px;
width: 100%;
overflow: hidden;
}
.usage-bar-fill {
height: 100%;
border-radius: 3px;
background: linear-gradient(90deg, var(--secondary) 0%, var(--primary) 100%);
}
.usage-bar-text {
font-size: 0.75rem;
color: var(--text-muted);
font-family: var(--font-en);
display: flex;
justify-content: space-between;
}
/* Modal Overlay & Card styling */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(3, 7, 18, 0.8);
backdrop-filter: blur(12px);
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
}
.modal-overlay.active {
opacity: 1;
pointer-events: auto;
}
.modal-content {
background: #0b0f19;
border: 1px solid var(--card-border-hover);
border-radius: 24px;
max-width: 500px;
width: 90%;
padding: 2.25rem;
transform: scale(0.95);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
}
.modal-overlay.active .modal-content {
transform: scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-close {
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 1.5rem;
}
.modal-close:hover {
color: var(--text-main);
}
.form-group {
margin-bottom: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
}
.form-input, select {
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--card-border);
border-radius: 10px;
padding: 0.75rem 1rem;
color: var(--text-main);
outline: none;
font-family: inherit;
width: 100%;
}
.form-input:focus, select:focus {
border-color: var(--primary);
background: rgba(255, 255, 255, 0.08);
}
/* Multi-Language Custom Font Handling */
[dir="ltr"] body {
font-family: var(--font-en);
}
/* Toast Notifications */
.toast {
position: fixed;
bottom: 2rem;
left: 2rem;
background: rgba(17, 24, 39, 0.9);
border-left: 4px solid var(--success);
padding: 1rem 1.5rem;
border-radius: 12px;
color: var(--text-main);
box-shadow: 0 10px 25px rgba(0,0,0,0.4);
backdrop-filter: blur(8px);
z-index: 300;
display: flex;
align-items: center;
gap: 0.75rem;
transform: translateY(150%);
opacity: 0;
}
[dir="rtl"] .toast {
left: auto;
right: 2rem;
border-left: none;
border-right: 4px solid var(--success);
}
.toast.error {
border-color: var(--danger);
}
.toast.active {
transform: translateY(0);
opacity: 1;
}
</style>
</head>
<body x-data="superAdminDashboard()" x-init="initDashboard()">
<!-- Ambient glowing backgrounds -->
<div class="ambient-glows">
<div class="glow-1"></div>
<div class="glow-2"></div>
</div>
<!-- Navigation Header -->
<header>
<div class="nav-container">
<a href="/admin" class="logo-section">
<svg width="36" height="36" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="14" fill="url(#paint0_linear_logo)" />
<path d="M14 26C14 20.4772 18.4772 16 24 16C29.5228 16 34 20.4772 34 26C34 31.5228 29.5228 36 24 36" stroke="white" stroke-width="3" stroke-linecap="round" />
<circle cx="24" cy="26" r="4" fill="white" />
<path d="M21 13L24 10L27 13" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
<defs>
<linearGradient id="paint0_linear_logo" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
<stop stop-color="#818cf8" />
<stop offset="1" stop-color="#4f46e5" />
</linearGradient>
</defs>
</svg>
<span class="logo-text">نابه <span style="font-size: 0.75rem; font-weight: 500; color: var(--secondary);">SuperAdmin</span></span>
</a>
<div class="nav-actions">
<button class="btn btn-glass" @click="toggleLanguage()">
<span x-text="lang === 'ar' ? 'English' : 'العربية'"></span>
</button>
<button class="btn btn-danger" @click="logout()">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
<span x-text="t('logout')">تسجيل الخروج</span>
</button>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="main-container">
<!-- Platform statistics -->
<section class="stats-grid">
<div class="stat-card">
<div class="stat-info">
<span class="stat-title" x-text="t('total_companies')">إجمالي الشركات</span>
<span class="stat-value" x-text="stats.total_companies">-</span>
</div>
<div class="stat-icon">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5m0 0V11m0 5H9m4-3h2m-2 0h-5m-9 0H3m2" />
</svg>
</div>
</div>
<div class="stat-card">
<div class="stat-info">
<span class="stat-title" x-text="t('active_connections')">الجلسات المتصلة</span>
<span class="stat-value" x-text="stats.connected_sessions + ' / ' + stats.total_sessions">-</span>
</div>
<div class="stat-icon" style="background: rgba(14, 165, 233, 0.1); border-color: rgba(14, 165, 233, 0.2); color: var(--secondary)">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
</div>
<div class="stat-card">
<div class="stat-info">
<span class="stat-title" x-text="t('system_status')">حالة النظام</span>
<span class="stat-value" style="font-size: 1.4rem; font-weight: 700; color: var(--success); margin-top: 0.5rem;" x-text="t('operational')">مستقر</span>
</div>
<div class="stat-icon" style="background: rgba(16, 185, 129, 0.1); border-color: rgba(16, 185, 129, 0.2); color: var(--success)">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</section>
<!-- Tenancy List -->
<section class="card">
<div class="card-header">
<div class="card-title">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="color: var(--primary);">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<span x-text="t('companies_list')">قائمة الشركات والاشتراكات</span>
</div>
<div class="search-box">
<input type="text" x-model="searchQuery" :placeholder="t('search_placeholder')">
</div>
</div>
<div class="table-responsive">
<table>
<thead>
<tr>
<th x-text="t('col_id')">ID</th>
<th x-text="t('col_company')">الشركة</th>
<th x-text="t('col_plan')">الباقة</th>
<th x-text="t('col_sessions')">الأرقام النشطة</th>
<th x-text="t('col_usage_api')">رسائل الـ API</th>
<th x-text="t('col_usage_voice')">الرسائل الصوتية</th>
<th x-text="t('col_ends_at')">تاريخ الانتهاء</th>
<th x-text="t('col_actions')">إجراءات</th>
</tr>
</thead>
<tbody>
<template x-for="company in filteredCompanies()" :key="company.id">
<tr>
<td style="font-family: var(--font-en); font-weight: 600;" x-text="company.id"></td>
<td>
<div style="font-weight: 600;" x-text="company.name"></div>
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.15rem;" x-text="company.status"></div>
</td>
<td>
<span class="badge"
:class="company.plan_name ? 'badge-primary' : 'badge-danger'"
x-text="company.plan_name ? company.plan_name : t('no_active_plan')">
</span>
</td>
<td>
<span class="badge badge-success" x-text="company.active_sessions + ' / ' + company.sessions_count"></span>
</td>
<td>
<!-- Request Usage -->
<div class="usage-bar-container">
<div class="usage-bar-track">
<div class="usage-bar-fill" :style="'width: ' + getUsagePercentage(company.request_usage, company.id === 1 ? 999999 : (company.plan_id == 1 ? 1000 : (company.plan_id == 2 ? 5000 : 20000))) + '%'"></div>
</div>
<div class="usage-bar-text">
<span x-text="company.request_usage"></span>
<span x-text="company.id === 1 ? '∞' : (company.plan_id == 1 ? '1k' : (company.plan_id == 2 ? '5k' : '20k'))"></span>
</div>
</div>
</td>
<td>
<!-- Voice Notes Usage -->
<div class="usage-bar-container">
<div class="usage-bar-track">
<div class="usage-bar-fill" style="background: linear-gradient(90deg, #38bdf8 0%, #0ea5e9 100%);" :style="'width: ' + getUsagePercentage(company.voice_usage, company.id === 1 ? 999999 : (company.plan_id == 1 ? 0 : (company.plan_id == 2 ? 100 : 500))) + '%'"></div>
</div>
<div class="usage-bar-text">
<span x-text="company.voice_usage"></span>
<span x-text="company.id === 1 ? '∞' : (company.plan_id == 1 ? '0' : (company.plan_id == 2 ? '100' : '500'))"></span>
</div>
</div>
</td>
<td>
<span style="font-family: var(--font-en); font-size: 0.85rem;"
x-text="company.subscription_ends ? formatDate(company.subscription_ends) : '-'">
</span>
</td>
<td>
<button class="btn btn-glass" style="padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="openSubscriptionModal(company)">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="margin: 0;">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
<span x-text="t('edit_plan')">تحديث الاشتراك</span>
</button>
</td>
</tr>
</template>
<tr x-show="filteredCompanies().length === 0">
<td colspan="8" style="text-align: center; color: var(--text-muted); padding: 3rem;" x-text="t('no_results')">لا توجد نتائج مطابقة للبحث</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
<!-- Upgrade / Modify Plan Subscription Modal -->
<div class="modal-overlay" :class="isModalOpen ? 'active' : ''" @click.self="closeSubscriptionModal()">
<div class="modal-content">
<div class="modal-header">
<h3 style="font-weight: 800; font-size: 1.2rem;" x-text="t('modal_title') + ': ' + selectedCompany?.name">تحديث اشتراك الشركة</h3>
<button class="modal-close" @click="closeSubscriptionModal()">&times;</button>
</div>
<form @submit.prevent="submitSubscription()">
<div class="form-group">
<label x-text="t('select_plan')">اختر الباقة</label>
<select x-model="modalData.plan_id" required>
<template x-for="plan in plans" :key="plan.id">
<option :value="plan.id" x-text="plan.name + ' (' + plan.price + '$ / ' + plan.max_sessions + ' ' + t('sessions_label') + ')'"></option>
</template>
</select>
</div>
<div class="form-group">
<label x-text="t('duration_days')">مدة الاشتراك بالأيام</label>
<input type="number" min="1" max="1000" class="form-input" x-model.number="modalData.duration_days" required>
</div>
<div style="margin-top: 2rem; display: flex; gap: 1rem; justify-content: flex-end;">
<button type="button" class="btn btn-glass" @click="closeSubscriptionModal()" x-text="t('cancel')">إلغاء</button>
<button type="submit" class="btn btn-primary" :disabled="isSubmitting">
<span x-show="isSubmitting" x-text="t('saving')">جاري الحفظ...</span>
<span x-show="!isSubmitting" x-text="t('save_changes')">حفظ التغييرات</span>
</button>
</div>
</form>
</div>
</div>
<!-- Toast Notification alerts -->
<div class="toast" :class="{ 'active': toast.show, 'error': toast.isError }">
<svg x-show="!toast.isError" width="20" height="20" fill="none" stroke="var(--success)" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<svg x-show="toast.isError" width="20" height="20" fill="none" stroke="var(--danger)" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span x-text="toast.message"></span>
</div>
<!-- App Frontend Dashboard Controller Script -->
<script>
function superAdminDashboard() {
// Localized Bilingual Translations
const translations = {
ar: {
logout: 'تسجيل الخروج',
total_companies: 'إجمالي الشركات والعملاء',
active_connections: 'الجلسات والخطوط المتصلة',
system_status: 'حالة منصة نابه',
operational: 'يعمل بكفاءة',
companies_list: 'لوحة التحكم وإدارة الاشتراكات للشركات',
search_placeholder: 'ابحث باسم الشركة أو الـ ID...',
col_id: 'الرقم المرجعي',
col_company: 'الشركة',
col_plan: 'الباقة الحالية',
col_sessions: 'الأرقام النشطة',
col_usage_api: 'الرسائل النصية المستهلكة',
col_usage_voice: 'الرسائل الصوتية المستهلكة',
col_ends_at: 'تاريخ انتهاء الباقة',
col_actions: 'إجراءات',
no_active_plan: 'بدون باقة نشطة',
edit_plan: 'تحديث الاشتراك',
no_results: 'لم يتم العثور على أي نتائج مطابقة لبحثك.',
modal_title: 'تعديل وتحديث اشتراك شركة',
select_plan: 'اختر باقة الاشتراك الجديدة',
sessions_label: 'أرقام',
duration_days: 'مدة تفعيل الباقة (بالأيام)',
cancel: 'إلغاء الأمر',
save_changes: 'حفظ وترقية الباقة',
saving: 'جاري الحفظ والترقية...',
success_update: 'تم تحديث اشتراك الشركة وترقية الباقة بنجاح!',
fail_update: 'فشل تفعيل الاشتراك الجديد. يرجى المحاولة لاحقاً.'
},
en: {
logout: 'Log Out',
total_companies: 'Total Companies',
active_connections: 'Connected Sessions',
system_status: 'System Status',
operational: 'Operational',
companies_list: 'Platform Tenancy & Subscriptions',
search_placeholder: 'Search by company name or ID...',
col_id: 'ID',
col_company: 'Company',
col_plan: 'Active Plan',
col_sessions: 'Sessions Active',
col_usage_api: 'API Requests Usage',
col_usage_voice: 'Voice Notes Usage',
col_ends_at: 'Ends At',
col_actions: 'Actions',
no_active_plan: 'No active subscription',
edit_plan: 'Edit Subscription',
no_results: 'No companies match your search queries.',
modal_title: 'Edit Subscription for',
select_plan: 'Choose Subscription Plan',
sessions_label: 'sessions',
duration_days: 'Subscription Duration (Days)',
cancel: 'Cancel',
save_changes: 'Apply Subscription',
saving: 'Updating...',
success_update: 'Company subscription plan updated successfully!',
fail_update: 'Failed to update subscription. Please try again.'
}
};
return {
lang: localStorage.getItem('nabeh_admin_lang') || 'ar',
token: localStorage.getItem('nabeh_token') || '',
user: null,
stats: {
total_companies: 0,
total_sessions: 0,
connected_sessions: 0
},
companies: [],
plans: [],
searchQuery: '',
isModalOpen: false,
selectedCompany: null,
modalData: {
plan_id: null,
duration_days: 30
},
isSubmitting: false,
toast: {
show: false,
message: '',
isError: false
},
initDashboard() {
this.lang = localStorage.getItem('nabeh_admin_lang') || 'ar';
document.documentElement.dir = this.lang === 'ar' ? 'rtl' : 'ltr';
document.documentElement.lang = this.lang;
// Verify Auth & Admin Privileges
const storedUser = localStorage.getItem('nabeh_user');
if (!this.token || !storedUser) {
this.redirectToLogin();
return;
}
try {
this.user = JSON.parse(storedUser);
if (parseInt(this.user.company_id) !== 1 || this.user.role !== 'admin') {
this.redirectToLogin();
return;
}
} catch (e) {
this.redirectToLogin();
return;
}
// Load platform stats & plans
this.fetchStats();
},
toggleLanguage() {
this.lang = this.lang === 'ar' ? 'en' : 'ar';
localStorage.setItem('nabeh_admin_lang', this.lang);
document.documentElement.dir = this.lang === 'ar' ? 'rtl' : 'ltr';
document.documentElement.lang = this.lang;
},
t(key) {
return translations[this.lang][key] || key;
},
redirectToLogin() {
localStorage.removeItem('nabeh_token');
localStorage.removeItem('nabeh_user');
window.location.href = '/';
},
logout() {
this.redirectToLogin();
},
async fetchStats() {
try {
const response = await fetch('/api/admin/stats', {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json'
}
});
if (response.status === 401 || response.status === 403) {
this.redirectToLogin();
return;
}
const result = await response.json();
if (result.status === 'success') {
this.stats = result.data.stats;
this.companies = result.data.companies;
this.plans = result.data.plans;
} else {
this.showToast(result.error || 'Failed to load stats', true);
}
} catch (err) {
this.showToast('Network error loading admin stats', true);
}
},
filteredCompanies() {
if (!this.searchQuery) return this.companies;
const query = this.searchQuery.toLowerCase().trim();
return this.companies.filter(c =>
c.name.toLowerCase().includes(query) ||
c.id.toString() === query
);
},
getUsagePercentage(usage, limit) {
if (!limit) return 0;
if (limit === 999999) return Math.min((usage / 20000) * 100, 100); // Admin Tenancy cap visual at 20k
return Math.min((usage / limit) * 100, 100);
},
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString(this.lang === 'ar' ? 'ar-EG' : 'en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
},
openSubscriptionModal(company) {
this.selectedCompany = company;
this.modalData.plan_id = company.plan_id || (this.plans[0]?.id || 1);
this.modalData.duration_days = 30;
this.isModalOpen = true;
},
closeSubscriptionModal() {
this.isModalOpen = false;
this.selectedCompany = null;
},
async submitSubscription() {
if (!this.selectedCompany) return;
this.isSubmitting = true;
try {
const response = await fetch('/api/admin/companies/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({
company_id: this.selectedCompany.id,
plan_id: this.modalData.plan_id,
duration_days: this.modalData.duration_days
})
});
const result = await response.json();
this.isSubmitting = false;
if (result.status === 'success') {
this.showToast(this.t('success_update'), false);
this.closeSubscriptionModal();
this.fetchStats(); // refresh database records visual
} else {
this.showToast(result.error || this.t('fail_update'), true);
}
} catch (err) {
this.isSubmitting = false;
this.showToast('Network error while updating subscription', true);
}
},
showToast(message, isError) {
this.toast.message = message;
this.toast.isError = isError;
this.toast.show = true;
setTimeout(() => {
this.toast.show = false;
}, 4000);
}
};
}
</script>
</body>
</html>