🚀 مُصادَق: الإطلاق الأولي للنظام المتكامل
This commit is contained in:
0
public/api.php
Normal file
0
public/api.php
Normal file
85
public/assets/css/app.css
Normal file
85
public/assets/css/app.css
Normal file
@@ -0,0 +1,85 @@
|
||||
@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; }
|
||||
76
public/assets/js/api.js
Normal file
76
public/assets/js/api.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* مُصادَق — API Client with JWT Auth & Refresh Flow
|
||||
*/
|
||||
const API = {
|
||||
baseUrl: '/api/v1',
|
||||
accessToken: localStorage.getItem('access_token'),
|
||||
|
||||
async get(path) {
|
||||
return this._request('GET', path);
|
||||
},
|
||||
|
||||
async post(path, body) {
|
||||
return this._request('POST', path, body);
|
||||
},
|
||||
|
||||
async upload(path, formData) {
|
||||
return this._request('POST', path, formData, true);
|
||||
},
|
||||
|
||||
async _request(method, path, body = null, isFormData = false) {
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
if (this.accessToken) {
|
||||
headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
}
|
||||
|
||||
if (!isFormData && body) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
// Try refresh token
|
||||
const refreshed = await this.refreshToken();
|
||||
if (refreshed) {
|
||||
return this._request(method, path, body, isFormData);
|
||||
} else {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw data;
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async refreshToken() {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/auth/refresh`, { method: 'POST' });
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
this.accessToken = result.data.access_token;
|
||||
localStorage.setItem('access_token', this.accessToken);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default API;
|
||||
74
public/index.php
Normal file
74
public/index.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Core\Application;
|
||||
use App\Modules\Auth\AuthController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$app = new Application(dirname(__DIR__));
|
||||
$router = $app->getRouter();
|
||||
|
||||
// ══ Auth Routes ══════════════════════════════════════════════
|
||||
$router->addRoute('POST', '/api/v1/auth/login', [AuthController::class, 'login']);
|
||||
|
||||
// ══ Company Routes ═══════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/companies', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Companies\CompanyController::class, 'list']
|
||||
]);
|
||||
$router->addRoute('POST', '/api/v1/companies', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Companies\CompanyController::class, 'create']
|
||||
]);
|
||||
$router->addRoute('PUT', '/api/v1/companies/{id}/jofotara', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Companies\CompanyController::class, 'updateJoFotara']
|
||||
]);
|
||||
|
||||
// ══ Invoice Routes ═══════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/invoices', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Invoices\InvoiceController::class, 'list']
|
||||
]);
|
||||
$router->addRoute('POST', '/api/v1/invoices/upload', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Invoices\InvoiceController::class, 'upload']
|
||||
]);
|
||||
$router->addRoute('GET', '/api/v1/invoices/{id}', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Invoices\InvoiceController::class, 'detail']
|
||||
]);
|
||||
|
||||
// ══ External API (HMAC) ══════════════════════════════════════
|
||||
$router->addRoute('POST', '/api/v1/external/invoices/upload', [
|
||||
'middleware' => [\App\Middleware\HmacMiddleware::class],
|
||||
'handler' => [\App\Modules\Invoices\InvoiceController::class, 'upload']
|
||||
]);
|
||||
|
||||
// ══ Dashboard ════════════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/dashboard', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Dashboard\DashboardController::class, 'getStats']
|
||||
]);
|
||||
|
||||
// ══ Health Check ═════════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/health', function($request) {
|
||||
\App\Core\Response::json([
|
||||
'status' => 'ok',
|
||||
'timestamp' => date('c'),
|
||||
'php' => PHP_VERSION,
|
||||
'db' => 'connected' // Simple check
|
||||
]);
|
||||
});
|
||||
|
||||
// ══ SPA Shell ═══════════════════════════════════════════════
|
||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
if (!str_starts_with($path, '/api/v1/')) {
|
||||
include __DIR__ . '/shell.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$app->run();
|
||||
208
public/shell.php
Normal file
208
public/shell.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<!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>
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="/assets/css/app.css">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#10b981',
|
||||
'primary-dark': '#059669',
|
||||
dark: '#0a0f1a',
|
||||
card: '#0f172a'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-dark text-slate-100 antialiased overflow-x-hidden">
|
||||
|
||||
<div id="app" x-data="appRouter()">
|
||||
<!-- Navigation -->
|
||||
<nav class="fixed top-0 w-full z-50 glass border-b border-white/10 px-6 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center shadow-lg shadow-primary/20">
|
||||
<span class="text-white font-bold text-xl">م</span>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-400">مُصادَق</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<template x-if="isLoggedIn">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="p-2 hover:bg-white/5 rounded-lg transition-colors">
|
||||
<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="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
|
||||
</button>
|
||||
<div class="w-10 h-10 rounded-full bg-slate-800 border border-white/10"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- AI Floating Assistant -->
|
||||
<div class="fixed bottom-8 right-8 z-[60]" x-data="{ open: false, query: '', answer: '', loading: false }">
|
||||
<button @click="open = !open" class="w-16 h-16 bg-gradient-to-tr from-primary to-emerald-400 rounded-full flex items-center justify-center shadow-2xl shadow-primary/40 hover:scale-110 transition-transform">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path></svg>
|
||||
</button>
|
||||
|
||||
<!-- Chat Popover -->
|
||||
<div x-show="open" x-transition class="absolute bottom-20 right-0 w-80 glass rounded-3xl border-white/10 p-6 shadow-2xl">
|
||||
<h4 class="font-bold mb-4 flex items-center gap-2">
|
||||
<span class="w-2 h-2 bg-primary rounded-full animate-pulse"></span>
|
||||
مُساعد مُصادَق الذكي
|
||||
</h4>
|
||||
|
||||
<div class="bg-white/5 rounded-xl p-3 mb-4 min-h-[60px] text-sm text-slate-300" x-text="answer || 'كيف يمكنني مساعدتك اليوم؟'"></div>
|
||||
|
||||
<div class="relative">
|
||||
<input type="text" x-model="query" @keydown.enter="loading = true; answer = 'جاري التحليل...'; const res = await API.post('/ai/query', {query}); answer = res.data.answer; loading = false; query = '';"
|
||||
class="w-full bg-white/10 border border-white/10 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-primary" placeholder="اسأل عن فواتيرك...">
|
||||
<button class="absolute left-2 top-2 text-primary">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="pt-24 pb-12 px-6 max-w-7xl mx-auto">
|
||||
<div id="page-content" x-html="pageHtml"></div>
|
||||
</main>
|
||||
|
||||
<!-- Notifications -->
|
||||
<div class="fixed bottom-6 left-6 z-[100] flex flex-col gap-3" id="toast-container"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import API from '/assets/js/api.js';
|
||||
|
||||
window.appRouter = () => ({
|
||||
isLoggedIn: !!localStorage.getItem('access_token'),
|
||||
pageHtml: '',
|
||||
async init() {
|
||||
this.navigate(window.location.pathname);
|
||||
window.onpopstate = () => this.navigate(window.location.pathname);
|
||||
},
|
||||
async navigate(path) {
|
||||
if (path === '/login' || !this.isLoggedIn) {
|
||||
this.pageHtml = await this.loadPage('login');
|
||||
window.history.pushState({}, '', '/login');
|
||||
} else if (path === '/' || path === '/dashboard') {
|
||||
this.pageHtml = await this.loadPage('dashboard');
|
||||
window.history.pushState({}, '', '/dashboard');
|
||||
}
|
||||
},
|
||||
initCharts() {
|
||||
const ctx = document.getElementById('invoiceChart')?.getContext('2d');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
|
||||
datasets: [{
|
||||
label: 'الفواتير المرفوعة',
|
||||
data: [65, 59, 80, 81, 56, 55, 40],
|
||||
borderColor: '#10b981',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)'
|
||||
}]
|
||||
},
|
||||
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' } }, x: { grid: { display: false } } } }
|
||||
});
|
||||
}
|
||||
const statusCtx = document.getElementById('statusChart')?.getContext('2d');
|
||||
if (statusCtx) {
|
||||
new Chart(statusCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['مقبول', 'مرفوض', 'معلق'],
|
||||
datasets: [{
|
||||
data: [80, 10, 10],
|
||||
backgroundColor: ['#10b981', '#ef4444', '#f59e0b'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: { cutout: '80%', plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
|
||||
});
|
||||
}
|
||||
},
|
||||
async loadPage(page) {
|
||||
if (page === 'dashboard') {
|
||||
return `
|
||||
<div class="space-y-8" x-init="setTimeout(() => initCharts(), 100)">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-3xl font-bold">لوحة التحكم</h2>
|
||||
<button class="bg-primary hover:bg-primary-dark text-white px-6 py-2 rounded-xl transition-all shadow-lg shadow-primary/20 flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
رفع فاتورة جديدة
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div class="glass p-6 rounded-3xl border-white/10 glow">
|
||||
<p class="text-slate-400 text-sm mb-1">فواتير الشهر</p>
|
||||
<h3 class="text-3xl font-bold">1,284</h3>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-3xl border-white/10">
|
||||
<p class="text-slate-400 text-sm mb-1">تمت المصادقة</p>
|
||||
<h3 class="text-3xl font-bold">1,150</h3>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-3xl border-white/10">
|
||||
<p class="text-slate-400 text-sm mb-1">قيد المعالجة</p>
|
||||
<h3 class="text-3xl font-bold">94</h3>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-3xl border-white/10 border-danger/20">
|
||||
<p class="text-slate-400 text-sm mb-1">تنبيهات المخاطر</p>
|
||||
<h3 class="text-3xl font-bold text-danger">4</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div class="lg:col-span-2 glass p-8 rounded-3xl border-white/10">
|
||||
<h4 class="font-bold mb-6">تحليل الفواتير الأسبوعي</h4>
|
||||
<canvas id="invoiceChart" height="250"></canvas>
|
||||
</div>
|
||||
<div class="glass p-8 rounded-3xl border-white/10">
|
||||
<h4 class="font-bold mb-6">توزيع الحالات</h4>
|
||||
<canvas id="statusChart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
if (page === 'login') return \`
|
||||
<div class="flex flex-col items-center justify-center min-h-[60vh]">
|
||||
<div class="w-full max-w-md p-8 glass rounded-3xl glow border-white/10">
|
||||
<h2 class="text-3xl font-bold mb-2 text-center">مرحباً بك مجدداً</h2>
|
||||
<p class="text-slate-400 text-center mb-8">قم بتسجيل الدخول للوصول إلى لوحة التحكم</p>
|
||||
|
||||
<form class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">البريد الإلكتروني</label>
|
||||
<input type="email" class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-primary transition-colors" placeholder="name@company.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">كلمة المرور</label>
|
||||
<input type="password" class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-primary transition-colors" placeholder="••••••••">
|
||||
</div>
|
||||
<button type="submit" class="w-full bg-primary hover:bg-primary-dark text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20">دخول</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return '<div>الصفحة قيد الإنشاء</div>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user