Update: 2026-05-03 17:32:57

This commit is contained in:
Hamza-Ayed
2026-05-03 17:32:57 +03:00
parent 6a3e66ad49
commit 4b40b1185f
102 changed files with 525 additions and 11371 deletions

View File

@@ -1,85 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@400;500;600;700&family=JetBrains+Mono&family=Inter:wght@400;500;600&display=swap');
:root {
--primary: #10b981;
--primary-hover: #059669;
--primary-muted: rgba(16,185,129,0.1);
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
--success: #22c55e;
/* Dark (default) */
--bg-app: #0a0f1a;
--bg-card: rgba(15,23,42,0.8);
--bg-sidebar: #060b14;
--bg-input: rgba(15,23,42,0.6);
--border: rgba(51,65,85,0.6);
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #475569;
--glass: rgba(15,23,42,0.6);
--glass-border: rgba(255,255,255,0.06);
--shadow-glow: 0 0 40px rgba(16,185,129,0.08);
}
[data-theme="light"] {
--bg-app: #f1f5f9;
--bg-card: #ffffff;
--bg-sidebar: #ffffff;
--bg-input: #f8fafc;
--border: #e2e8f0;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--glass: rgba(255,255,255,0.8);
--glass-border: rgba(0,0,0,0.04);
--shadow-glow: 0 4px 24px rgba(0,0,0,0.06);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', 'IBM Plex Sans Arabic', sans-serif;
}
body {
background-color: var(--bg-app);
color: var(--text-primary);
direction: rtl;
min-height: 100vh;
overflow-x: hidden;
transition: background-color 0.3s, color 0.3s;
}
/* Glassmorphism Utilities */
.glass {
background: var(--glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
}
.glow {
box-shadow: var(--shadow-glow);
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* RTL Specifics */
[dir="rtl"] .ml-auto { margin-right: auto; margin-left: 0; }
[dir="rtl"] .mr-auto { margin-left: auto; margin-right: 0; }

View File

@@ -1,55 +0,0 @@
const API = {
baseUrl: '/api/v1',
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const token = localStorage.getItem('access_token');
const headers = {
'Accept': 'application/json',
...(options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }),
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
...options.headers
};
const response = await fetch(url, { ...options, headers });
if (response.status === 401 && !options._retry) {
// Attempt token refresh
const refreshed = await this.refresh();
if (refreshed) {
return this.request(endpoint, { ...options, _retry: true });
}
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'حدث خطأ ما');
}
return data;
},
async login(email, password) {
const data = await this.request('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
localStorage.setItem('access_token', data.data.access_token);
return data;
},
async refresh() {
try {
const data = await fetch(`${this.baseUrl}/auth/refresh`, { method: 'POST' });
if (data.ok) {
const result = await data.json();
localStorage.setItem('access_token', result.data.access_token);
return true;
}
} catch (e) {
console.error('Refresh failed', e);
}
localStorage.removeItem('access_token');
return false;
}
};

View File

@@ -1,102 +0,0 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl" x-data="{ darkMode: true }" :class="{ 'dark': darkMode }">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>مُصادَق — أتمتة الفواتير الضريبية</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Noto+Sans+Arabic:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
[x-cloak] { display: none !important; }
body { font-family: 'Noto Sans Arabic', 'Outfit', sans-serif; }
.glass { background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); }
.dark .glass { background: rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.05); }
</style>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e' },
accent: '#FFD700'
}
}
}
}
</script>
</head>
<body class="bg-gray-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 min-h-screen transition-colors duration-500 overflow-x-hidden">
<!-- Navbar -->
<nav class="sticky top-0 z-50 glass px-6 py-4 flex justify-between items-center mx-4 mt-4 rounded-2xl shadow-xl">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-primary-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-primary-500/30">
<span class="text-white font-bold text-xl">م</span>
</div>
<h1 class="text-2xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-primary-600 to-indigo-500 dark:from-primary-400 dark:to-indigo-400">مُصادَق</h1>
</div>
<div class="flex items-center gap-4">
<button @click="darkMode = !darkMode" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-slate-800 transition-all">
<template x-if="!darkMode">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>
</template>
<template x-if="darkMode">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M16.071 16.071l.707.707M7.929 7.929l.707-.707M12 8a4 4 0 100 8 4 4 0 000-8z"></path></svg>
</template>
</button>
<a href="index.php" class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-xl font-semibold transition-all shadow-lg shadow-primary-600/20">دخول</a>
</div>
</nav>
<!-- Main Content -->
<main class="container mx-auto px-4 py-12">
<section class="text-center py-20 relative">
<div class="absolute -top-20 left-1/2 -translate-x-1/2 w-64 h-64 bg-primary-500/20 blur-[100px] rounded-full"></div>
<h2 class="text-5xl md:text-7xl font-extrabold mb-6 leading-tight">
أتمتة <span class="text-primary-500">الفواتير</span> <br>بذكاء اصطناعي فائق
</h2>
<p class="text-xl text-slate-600 dark:text-slate-400 max-w-2xl mx-auto mb-10 leading-relaxed">
مُصادَق هو شريكك التقني المعتمد للربط مع نظام "جوفوتارا" الأردني، استخرج بيانات فواتيرك آلياً وامتثل للأنظمة الضريبية بثوانٍ.
</p>
<div class="flex flex-wrap justify-center gap-4">
<button class="px-10 py-4 bg-slate-900 dark:bg-white dark:text-slate-900 text-white rounded-2xl font-bold text-lg hover:scale-105 transition-all shadow-2xl">ابدأ التجربة المجانية</button>
<button class="px-10 py-4 glass rounded-2xl font-bold text-lg hover:bg-gray-100 dark:hover:bg-slate-800 transition-all">شاهد العرض</button>
</div>
</section>
<!-- Features Grid -->
<section class="grid md:grid-cols-3 gap-8 py-20">
<div class="p-8 glass rounded-3xl hover:-translate-y-2 transition-all duration-300">
<div class="w-14 h-14 bg-blue-100 dark:bg-blue-900/30 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
</div>
<h3 class="text-xl font-bold mb-3">استخراج ذكي (OCR)</h3>
<p class="text-slate-500">استخدام Gemini 2.0 لاستخراج كافة بنود الفواتير من الصور والـ PDF بدقة تصل لـ 99%.</p>
</div>
<div class="p-8 glass rounded-3xl hover:-translate-y-2 transition-all duration-300">
<div class="w-14 h-14 bg-green-100 dark:bg-green-900/30 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
</div>
<h3 class="text-xl font-bold mb-3">توافق جو-فواتير</h3>
<p class="text-slate-500">ربط مباشر مع منصة الفوترة الوطنية الأردنية وإصدار ملفات UBL 2.1 المعتمدة.</p>
</div>
<div class="p-8 glass rounded-3xl hover:-translate-y-2 transition-all duration-300">
<div class="w-14 h-14 bg-purple-100 dark:bg-purple-900/30 rounded-2xl flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
</div>
<h3 class="text-xl font-bold mb-3">حماية البيانات</h3>
<p class="text-slate-500">تشفير AES-256 للبيانات الحساسة وعزل كامل لبيانات المستأجرين (Multi-tenancy).</p>
</div>
</section>
</main>
<footer class="py-10 text-center text-slate-500 text-sm">
<p>© 2026 مُصادَق — جميع الحقوق محفوظة لشركة انتاليك للحلول البرمجية</p>
</footer>
<script src="assets/js/api.js"></script>
</body>
</html>

View File

@@ -1,152 +1,36 @@
<?php
/**
* Simple Router & Entry Point
*/
declare(strict_types=1);
require_once __DIR__ . '/../app/bootstrap/init.php';
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../app/Core/helpers.php';
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$route = $_GET['route'] ?? str_replace('/api/', '', $uri);
$route = trim($route, '/');
use App\Core\Application;
use App\Modules\Auth\AuthController;
use App\Modules\Companies\CompanyController;
use App\Modules\Invoices\InvoiceController;
use App\Modules\Dashboard\DashboardController;
use App\Modules\Users\UsersController;
use App\Modules\ApiKeys\ApiKeyController;
use App\Modules\Admin\AdminController;
use App\Middleware\AuthMiddleware;
use App\Middleware\HmacMiddleware;
// Mapping routes to modules
$routes = [
'auth/login' => 'auth/login.php',
'auth/refresh' => 'auth/refresh.php',
'auth/logout' => 'auth/logout.php',
'users' => 'users/index.php',
'trips' => 'trips/index.php',
];
$app = new Application(dirname(__DIR__));
$router = $app->getRouter();
// ══ Auth Routes ══════════════════════════════════════════════
$router->addRoute('POST', '/api/v1/auth/login', [AuthController::class, 'login']);
$router->addRoute('POST', '/api/v1/auth/register', [AuthController::class, 'register']);
$router->addRoute('POST', '/api/v1/auth/refresh', [AuthController::class, 'refresh']);
$router->addRoute('POST', '/api/v1/auth/logout', [AuthController::class, 'logout']);
$router->addRoute('GET', '/api/v1/auth/me', [
'middleware' => [AuthMiddleware::class],
'handler' => [AuthController::class, 'me']
]);
$router->addRoute('POST', '/api/v1/auth/2fa/enable', [
'middleware' => [AuthMiddleware::class],
'handler' => [AuthController::class, 'enable2FA']
]);
$router->addRoute('POST', '/api/v1/auth/2fa/verify', [
'middleware' => [AuthMiddleware::class],
'handler' => [AuthController::class, 'verify2FA']
]);
$router->addRoute('POST', '/api/v1/auth/2fa/disable', [
'middleware' => [AuthMiddleware::class],
'handler' => [AuthController::class, 'disable2FA']
]);
// ══ Company Routes ═══════════════════════════════════════════
$router->addRoute('GET', '/api/v1/companies', [
'middleware' => [AuthMiddleware::class],
'handler' => [CompanyController::class, 'index']
]);
$router->addRoute('POST', '/api/v1/companies', [
'middleware' => [AuthMiddleware::class],
'handler' => [CompanyController::class, 'store']
]);
$router->addRoute('GET', '/api/v1/companies/{id}', [
'middleware' => [AuthMiddleware::class],
'handler' => [CompanyController::class, 'show']
]);
$router->addRoute('PUT', '/api/v1/companies/{id}', [
'middleware' => [AuthMiddleware::class],
'handler' => [CompanyController::class, 'update']
]);
$router->addRoute('DELETE', '/api/v1/companies/{id}', [
'middleware' => [AuthMiddleware::class],
'handler' => [CompanyController::class, 'destroy']
]);
// ══ User Routes ══════════════════════════════════════════════
$router->addRoute('GET', '/api/v1/users', [
'middleware' => [AuthMiddleware::class],
'handler' => [UsersController::class, 'list']
]);
$router->addRoute('POST', '/api/v1/users', [
'middleware' => [AuthMiddleware::class],
'handler' => [UsersController::class, 'create']
]);
$router->addRoute('PUT', '/api/v1/users/{id}', [
'middleware' => [AuthMiddleware::class],
'handler' => [UsersController::class, 'update']
]);
$router->addRoute('DELETE', '/api/v1/users/{id}', [
'middleware' => [AuthMiddleware::class],
'handler' => [UsersController::class, 'destroy']
]);
// ══ Invoice Routes ═══════════════════════════════════════════
$router->addRoute('GET', '/api/v1/invoices', [
'middleware' => [AuthMiddleware::class],
'handler' => [InvoiceController::class, 'index']
]);
$router->addRoute('POST', '/api/v1/invoices/upload', [
'middleware' => [AuthMiddleware::class],
'handler' => [InvoiceController::class, 'upload']
]);
$router->addRoute('GET', '/api/v1/invoices/{id}', [
'middleware' => [AuthMiddleware::class],
'handler' => [InvoiceController::class, 'show']
]);
$router->addRoute('GET', '/api/v1/invoices/{id}/status', [
'middleware' => [AuthMiddleware::class],
'handler' => [InvoiceController::class, 'status']
]);
$router->addRoute('GET', '/api/v1/invoices/{id}/file', [
'middleware' => [AuthMiddleware::class],
'handler' => [InvoiceController::class, 'serveFile']
]);
// ══ Dashboard ════════════════════════════════════════════════
$router->addRoute('GET', '/api/v1/dashboard', [
'middleware' => [AuthMiddleware::class],
'handler' => [DashboardController::class, 'getStats']
]);
// ══ API Keys ═══════════════════════════════════════════════════
$router->addRoute('GET', '/api/v1/api-keys', [
'middleware' => [AuthMiddleware::class],
'handler' => [ApiKeyController::class, 'index']
]);
$router->addRoute('POST', '/api/v1/api-keys', [
'middleware' => [AuthMiddleware::class],
'handler' => [ApiKeyController::class, 'create']
]);
$router->addRoute('DELETE', '/api/v1/api-keys/{id}', [
'middleware' => [AuthMiddleware::class],
'handler' => [ApiKeyController::class, 'revoke']
]);
// ══ Admin Routes (Super Admin) ════════════════════════════════
$router->addRoute('GET', '/api/v1/admin/tenants', [
'middleware' => [AuthMiddleware::class],
'handler' => [AdminController::class, 'listTenants']
]);
$router->addRoute('GET', '/api/v1/admin/stats', [
'middleware' => [AuthMiddleware::class],
'handler' => [AdminController::class, 'getSystemStats']
]);
$router->addRoute('GET', '/api/v1/admin/queue', [
'middleware' => [AuthMiddleware::class],
'handler' => [AdminController::class, 'getQueueStatus']
]);
// ══ Health & Public ═══════════════════════════════════════════
$router->addRoute('GET', '/api/v1/health', [AdminController::class, 'health']);
// ══ Determine if this is an API request ═════════════════════════════
$apiRoute = $_GET['route'] ?? null;
if (!$apiRoute) {
// Not an API call — serve the SPA shell
include __DIR__ . '/shell.php';
exit;
if (isset($routes[$route])) {
$file = APP_PATH . '/modules_app/' . $routes[$route];
if (file_exists($file)) {
require_once $file;
} else {
json_error("Endpoint file missing: {$route}", 500);
}
} else {
// If no route matches, maybe it's a SPA request or 404
if (str_starts_with($route, 'v1/')) {
json_error("Not Found: {$route}", 404);
} else {
// Fallback for non-API requests (Frontend)
echo "<h1>Musadaq API - Pure PHP</h1><p>Running on simple architecture.</p>";
}
}
$app->run();

View File

@@ -1,476 +0,0 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>مُصادَق | أتمتة الفواتير الضريبية</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Tailwind CSS (via CDN for simplicity in this prototype) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Alpine.js -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
:root {
--emerald: #10b981;
--emerald-dim: rgba(16,185,129,0.12);
--emerald-border: rgba(16,185,129,0.25);
--bg-base: #080c14;
--bg-surface: #0d1424;
--bg-elevated: #111827;
--bg-hover: rgba(255,255,255,0.04);
--border-subtle: rgba(255,255,255,0.06);
--border-default: rgba(255,255,255,0.10);
--border-strong: rgba(255,255,255,0.18);
--text-primary: #f0f6fc;
--text-secondary: #8b949e;
--text-muted: #484f58;
--status-approved: #10b981;
--status-pending: #f59e0b;
--status-failed: #ef4444;
--status-processing: #6366f1;
}
[data-theme="light"] {
--bg-base: #f6f8fa;
--bg-surface: #ffffff;
--bg-elevated: #f0f3f7;
--bg-hover: rgba(0,0,0,0.04);
--border-subtle: rgba(0,0,0,0.05);
--border-default: rgba(0,0,0,0.10);
--text-primary: #0d1117;
--text-secondary: #57606a;
--text-muted: #afb8c1;
}
body {
font-family: 'IBM+Plex+Sans+Arabic', sans-serif;
background-color: var(--bg-base);
color: var(--text-primary);
margin: 0;
overflow: hidden;
}
.mono { font-family: 'IBM+Plex+Mono', monospace; }
/* Custom Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
#sidebar {
width: 260px;
background-color: var(--bg-surface);
border-left: 1px solid var(--border-default);
height: 100vh;
display: flex;
flex-direction: column;
z-index: 50;
}
#main-layout {
flex: 1;
height: 100vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.nav-link {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
color: var(--text-secondary);
transition: all 0.2s;
border-right: 3px solid transparent;
}
.nav-link:hover {
color: var(--text-primary);
background-color: var(--bg-hover);
}
.nav-active {
color: var(--emerald);
background-color: var(--emerald-dim);
border-right-color: var(--emerald);
}
.stat-card {
background-color: var(--bg-surface);
border: 1px solid var(--border-default);
padding: 1.5rem;
border-radius: 4px;
transition: transform 0.2s;
cursor: pointer;
}
.stat-card:hover {
transform: translateY(-2px);
border-color: var(--emerald-border);
}
#topbar {
background-color: var(--bg-base);
border-bottom: 1px solid var(--border-subtle);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Modal styling */
.modal-overlay {
background-color: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(4px);
}
.modal-content {
background-color: var(--bg-elevated);
border: 1px solid var(--border-strong);
max-width: 600px;
width: 90%;
border-radius: 8px;
}
.loading-bar {
height: 2px;
background: var(--emerald);
position: fixed;
top: 0;
left: 0;
z-index: 9999;
transition: width 0.3s ease;
}
</style>
</head>
<body x-data="musadaqApp" x-init="init()">
<div id="loading-progress" class="loading-bar" :style="'width: ' + progress + '%'" x-show="loading"></div>
<div class="flex h-screen w-full">
<!-- Sidebar -->
<aside id="sidebar" x-show="user">
<div class="p-6">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-emerald-500 rounded flex items-center justify-center text-white font-bold">م</div>
<h1 class="text-xl font-bold tracking-tight text-white">مُصادَق</h1>
</div>
</div>
<nav class="mt-4 flex-1 overflow-y-auto">
<template x-for="item in navItems" :key="item.page">
<a href="#"
class="nav-link"
:class="currentPage === item.page ? 'nav-active' : ''"
@click.prevent="navigate(item.page)"
x-show="item.roles.includes(user.role)">
<span x-html="item.icon" class="ml-3"></span>
<span x-text="item.label"></span>
</a>
</template>
</nav>
<div class="p-6 border-t border-gray-800">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center">
<span x-text="user?.name?.charAt(0) || 'U'"></span>
</div>
<div class="flex-1 overflow-hidden">
<p class="text-sm font-medium truncate" x-text="user?.name"></p>
<p class="text-xs text-gray-500 uppercase" x-text="user?.role"></p>
</div>
</div>
<button @click="logout()" class="w-full py-2 text-sm text-red-400 hover:bg-red-950 rounded transition">تسجيل الخروج</button>
</div>
</aside>
<!-- Main Content -->
<div id="main-layout" class="flex-1">
<header id="topbar" x-show="user">
<div>
<h2 class="text-lg font-semibold" x-text="pageTitle"></h2>
<p class="text-xs text-gray-500">نظام أتمتة الفواتير الرقمي</p>
</div>
<div class="flex items-center gap-4">
<button @click="themeToggle()" class="p-2 hover:bg-gray-800 rounded">🌓</button>
<div class="h-8 w-px bg-gray-800"></div>
<button class="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded text-sm font-medium transition" @click="openUploadModal()">+ فاتورة جديدة</button>
</div>
</header>
<main id="content" class="p-8 flex-1 overflow-y-auto">
<!-- Dynamic Content Injection -->
<div x-show="currentPage === 'dashboard'">
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="stat-card">
<p class="text-gray-500 text-sm mb-2">فواتير الشهر</p>
<h3 class="text-3xl font-bold mono" x-text="stats.invoices_this_month || 0"></h3>
</div>
<div class="stat-card">
<p class="text-gray-500 text-sm mb-2">فواتير معتمدة</p>
<h3 class="text-3xl font-bold mono text-emerald-500" x-text="stats.approved_invoices || 0"></h3>
</div>
<div class="stat-card">
<p class="text-gray-500 text-sm mb-2">عدد الشركات</p>
<h3 class="text-3xl font-bold mono" x-text="stats.companies_count || 0"></h3>
</div>
<div class="stat-card">
<p class="text-gray-500 text-sm mb-2">استهلاك الباقة</p>
<h3 class="text-3xl font-bold mono" x-text="(stats.subscription_usage_pct || 0) + '%'"></h3>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 bg-surface rounded p-6 border border-gray-800">
<h4 class="font-bold mb-4">آخر الفواتير</h4>
<div class="overflow-x-auto">
<table class="w-full text-sm text-right">
<thead>
<tr class="text-gray-500 border-b border-gray-800">
<th class="pb-3 pr-2">الشركة</th>
<th class="pb-3">الرقم</th>
<th class="pb-3">التاريخ</th>
<th class="pb-3">الإجمالي</th>
<th class="pb-3">الحالة</th>
</tr>
</thead>
<tbody>
<template x-for="inv in stats.recent_invoices" :key="inv.id">
<tr class="border-b border-gray-900 hover:bg-gray-800/50 cursor-pointer" @click="navigate('invoice-detail', {id: inv.id})">
<td class="py-3 pr-2" x-text="inv.company_name"></td>
<td class="py-3 mono" x-text="inv.invoice_number"></td>
<td class="py-3" x-text="inv.invoice_date"></td>
<td class="py-3 mono font-bold" x-text="inv.grand_total + ' JOD'"></td>
<td class="py-3">
<span class="px-2 py-1 rounded-full text-xs"
:class="statusColors[inv.status]"
x-text="statusLabels[inv.status]"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<div class="bg-surface rounded p-6 border border-gray-800">
<h4 class="font-bold mb-4">المساعد الذكي</h4>
<div class="bg-gray-900/50 p-4 rounded mb-4">
<p class="text-xs text-gray-500 mb-2">🤖 اسأل عن بياناتك:</p>
<textarea class="w-full bg-transparent border-none text-sm resize-none focus:ring-0" placeholder="كم فاتورة رفعت الشهر الماضي؟"></textarea>
</div>
<button class="w-full py-2 bg-gray-800 hover:bg-gray-700 text-sm rounded transition">إرسال </button>
</div>
</div>
</div>
<!-- Companies List -->
<div x-show="currentPage === 'companies'">
<div class="flex justify-between items-center mb-8">
<h3 class="text-2xl font-bold">إدارة الشركات</h3>
<button class="bg-emerald-600 px-4 py-2 rounded text-sm" @click="openAddCompanyModal()">+ إضافة شركة</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<template x-for="comp in companies" :key="comp.id">
<div class="bg-surface p-6 rounded border border-gray-800 hover:border-emerald-900 transition">
<div class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 bg-gray-800 rounded flex items-center justify-center text-xl font-bold" x-text="comp.name.charAt(0)"></div>
<div>
<h4 class="font-bold text-lg" x-text="comp.name"></h4>
<p class="text-xs text-gray-500 mono" x-text="'TIN: ' + comp.tax_identification_number"></p>
</div>
</div>
<div class="flex gap-2 mt-4 pt-4 border-t border-gray-800">
<button class="px-3 py-1 bg-gray-800 rounded text-xs">إعدادات JoFotara</button>
<button class="px-3 py-1 bg-gray-800 rounded text-xs">تعديل</button>
</div>
</div>
</template>
</div>
</div>
<!-- Invoice List -->
<div x-show="currentPage === 'invoices'">
<div class="flex justify-between items-center mb-8">
<h3 class="text-2xl font-bold">الفواتير والتدقيق</h3>
</div>
<div class="bg-surface rounded border border-gray-800 overflow-hidden">
<table class="w-full text-sm text-right">
<thead class="bg-gray-900/50 text-gray-500 uppercase text-xs">
<tr>
<th class="p-4">الشركة</th>
<th class="p-4">الرقم</th>
<th class="p-4">التاريخ</th>
<th class="p-4">الإجمالي</th>
<th class="p-4">الحالة</th>
<th class="p-4">الثقة</th>
</tr>
</thead>
<tbody>
<template x-for="inv in invoices" :key="inv.id">
<tr class="border-t border-gray-800 hover:bg-gray-800/30 cursor-pointer" @click="navigate('invoice-detail', {id: inv.id})">
<td class="p-4" x-text="inv.company_name"></td>
<td class="p-4 mono" x-text="inv.invoice_number"></td>
<td class="p-4" x-text="inv.invoice_date"></td>
<td class="p-4 mono font-bold" x-text="inv.grand_total + ' JOD'"></td>
<td class="p-4">
<span class="px-2 py-1 rounded-full text-xs" :class="statusColors[inv.status]" x-text="statusLabels[inv.status]"></span>
</td>
<td class="p-4 mono">
<span :class="inv.ai_confidence_score < 0.7 ? 'text-red-500' : 'text-emerald-500'" x-text="(inv.ai_confidence_score * 100).toFixed(0) + '%'"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<!-- Modals -->
<div class="modal-overlay fixed inset-0 flex items-center justify-center z-[100]" x-show="showModal" x-cloak>
<div class="modal-content p-8" @click.outside="closeModal()">
<div id="modal-body"></div>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('musadaqApp', () => ({
user: JSON.parse(localStorage.getItem('user')),
currentPage: 'dashboard',
currentParams: {},
pageTitle: 'لوحة التحكم',
loading: false,
progress: 0,
showModal: false,
stats: {},
companies: [],
invoices: [],
navItems: [
{ page: 'dashboard', label: 'لوحة التحكم', icon: '📊', roles: ['admin', 'super_admin', 'accountant', 'viewer'] },
{ page: 'invoices', label: 'الفواتير', icon: '📄', roles: ['admin', 'super_admin', 'accountant', 'viewer'] },
{ page: 'companies', label: 'الشركات', icon: '🏢', roles: ['admin', 'super_admin'] },
{ page: 'staff', label: 'الموظفون', icon: '👥', roles: ['admin', 'super_admin'] },
{ page: 'settings', label: 'الإعدادات', icon: '⚙️', roles: ['admin', 'super_admin', 'accountant', 'viewer'] },
],
statusLabels: {
'uploaded': 'مرفوعة',
'extracting': 'جاري الاستخراج...',
'extracted': 'مستخرجة',
'validated': 'مدققة',
'approved': 'معتمدة ✓',
'rejected': 'مرفوضة ✗'
},
statusColors: {
'uploaded': 'bg-gray-700 text-gray-200',
'extracting': 'bg-indigo-900 text-indigo-200 animate-pulse',
'extracted': 'bg-blue-900 text-blue-200',
'validated': 'bg-cyan-900 text-cyan-200',
'approved': 'bg-emerald-900 text-emerald-200',
'rejected': 'bg-red-900 text-red-200'
},
async init() {
if (!this.user) {
window.location.href = '/login.php'; // Or handle login view
return;
}
this.navigate('dashboard');
},
async navigate(page, params = {}) {
this.currentPage = page;
this.currentParams = params;
this.pageTitle = this.navItems.find(i => i.page === page)?.label || 'التفاصيل';
this.loading = true;
this.progress = 30;
try {
if (page === 'dashboard') await this.loadStats();
if (page === 'companies') await this.loadCompanies();
if (page === 'invoices') await this.loadInvoices();
this.progress = 100;
setTimeout(() => { this.loading = false; this.progress = 0; }, 300);
} catch (e) {
console.error(e);
this.loading = false;
}
},
async loadStats() {
const res = await this.apiGet('/dashboard');
this.stats = res.data;
},
async loadCompanies() {
const res = await this.apiGet('/companies');
this.companies = res.data;
},
async loadInvoices() {
const res = await this.apiGet('/invoices');
this.invoices = res.data;
},
async apiGet(path) {
const res = await fetch('/api/v1' + path, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
});
if (res.status === 401) this.logout();
return await res.json();
},
logout() {
localStorage.clear();
window.location.reload();
},
themeToggle() {
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
},
openUploadModal() {
this.showModal = true;
document.getElementById('modal-body').innerHTML = `
<h2 class="text-xl font-bold mb-6">رفع فاتورة جديدة</h2>
<div class="space-y-4">
<div>
<label class="block text-sm text-gray-500 mb-1">الشركة</label>
<select class="w-full bg-gray-900 border border-gray-700 p-2 rounded">
<option>اختر الشركة...</option>
${this.companies.map(c => `<option value="${c.id}">${c.name}</option>`).join('')}
</select>
</div>
<div class="border-2 border-dashed border-gray-700 p-8 rounded text-center hover:border-emerald-500 transition cursor-pointer">
<span>📁 اسحب الملف هنا أو اضغط للاختيار</span>
</div>
<div class="flex justify-end gap-3 pt-4">
<button @click="closeModal()" class="px-4 py-2 text-sm text-gray-400">إلغاء</button>
<button class="px-6 py-2 bg-emerald-600 text-sm rounded font-bold">رفع ومعالجة</button>
</div>
</div>
`;
},
closeModal() {
this.showModal = false;
}
}));
});
</script>
</body>
</html>