Update: 2026-05-03 17:32:57
This commit is contained in:
@@ -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; }
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
176
public/index.php
176
public/index.php
@@ -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();
|
||||
|
||||
476
public/shell.php
476
public/shell.php
@@ -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>
|
||||
Reference in New Issue
Block a user