🚀 مُصادَق: تحديث برمجي جديد 2026-05-03 13:39
This commit is contained in:
@@ -47,7 +47,7 @@
|
||||
<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="#login" 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>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -15,6 +15,18 @@ $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/2fa/enable', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [AuthController::class, 'enable2FA']
|
||||
]);
|
||||
$router->addRoute('POST', '/api/v1/auth/2fa/verify', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [AuthController::class, 'verify2FA']
|
||||
]);
|
||||
$router->addRoute('POST', '/api/v1/auth/2fa/disable', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [AuthController::class, 'disable2FA']
|
||||
]);
|
||||
|
||||
// ══ Company Routes ═══════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/companies', [
|
||||
@@ -33,11 +45,11 @@ $router->addRoute('PUT', '/api/v1/companies/{id}/jofotara', [
|
||||
// ══ User Routes ══════════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/users', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Users\UsersController::class, 'list']
|
||||
'handler' => [\App\Modules\Users\UserController::class, 'index']
|
||||
]);
|
||||
$router->addRoute('POST', '/api/v1/users', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Users\UsersController::class, 'create']
|
||||
'handler' => [\App\Modules\Users\UserController::class, 'create']
|
||||
]);
|
||||
|
||||
// ══ Invoice Routes ═══════════════════════════════════════════
|
||||
@@ -53,6 +65,10 @@ $router->addRoute('GET', '/api/v1/invoices/{id}', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Invoices\InvoiceController::class, 'detail']
|
||||
]);
|
||||
$router->addRoute('POST', '/api/v1/invoices/{id}/submit', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Invoices\InvoiceController::class, 'submit']
|
||||
]);
|
||||
|
||||
// ══ Subscriptions ═════════════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/subscriptions/me', [
|
||||
@@ -60,6 +76,16 @@ $router->addRoute('GET', '/api/v1/subscriptions/me', [
|
||||
'handler' => [\App\Modules\Subscriptions\SubscriptionController::class, 'me']
|
||||
]);
|
||||
|
||||
// ══ API Keys ═══════════════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/api-keys', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class, \App\Middleware\TenantMiddleware::class],
|
||||
'handler' => [\App\Modules\ApiKeys\ApiKeyController::class, 'list']
|
||||
]);
|
||||
$router->addRoute('POST', '/api/v1/api-keys', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class, \App\Middleware\TenantMiddleware::class],
|
||||
'handler' => [\App\Modules\ApiKeys\ApiKeyController::class, 'create']
|
||||
]);
|
||||
|
||||
// ══ External API (HMAC) ══════════════════════════════════════
|
||||
$router->addRoute('POST', '/api/v1/external/invoices/upload', [
|
||||
'middleware' => [\App\Middleware\HmacMiddleware::class],
|
||||
@@ -72,6 +98,12 @@ $router->addRoute('GET', '/api/v1/dashboard', [
|
||||
'handler' => [\App\Modules\Dashboard\DashboardController::class, 'getStats']
|
||||
]);
|
||||
|
||||
// ══ Super Admin ══════════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/admin/stats', [
|
||||
'middleware' => [\App\Middleware\AuthMiddleware::class],
|
||||
'handler' => [\App\Modules\Admin\AdminController::class, 'getSystemStats']
|
||||
]);
|
||||
|
||||
// ══ Health Check ═════════════════════════════════════════════
|
||||
$router->addRoute('GET', '/api/v1/health', function($request) {
|
||||
\App\Core\Response::json([
|
||||
|
||||
187
public/shell.php
187
public/shell.php
@@ -326,6 +326,7 @@
|
||||
<div id="login-error" class="text-red-400 text-sm text-center hidden p-2 bg-red-500/10 rounded-lg border border-red-500/20"></div>
|
||||
<button type="submit" class="w-full bg-gradient-to-r from-primary to-emerald-400 hover:from-primary-dark hover:to-primary text-white font-bold py-3 rounded-xl shadow-lg transition-all">تسجيل الدخول</button>
|
||||
</form>
|
||||
<p class="mt-6 text-center text-slate-400 text-sm">ليس لديك حساب؟ <a href="#" onclick="renderRegister()" class="text-primary hover:underline">سجل شركتك الآن</a></p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -354,6 +355,62 @@
|
||||
};
|
||||
}
|
||||
|
||||
// ── Register View ───────────────────────────────────────────
|
||||
function renderRegister() {
|
||||
document.getElementById('sidebar').classList.add('hidden');
|
||||
document.getElementById('header').classList.add('hidden');
|
||||
document.getElementById('ai-container').classList.add('hidden');
|
||||
|
||||
contentDiv.innerHTML = `
|
||||
<div class="flex flex-col items-center justify-center min-h-[80vh] py-10">
|
||||
<div class="w-full max-w-lg p-10 glass-panel rounded-3xl shadow-2xl">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-emerald-400 to-primary rounded-2xl flex items-center justify-center shadow-lg shadow-emerald-500/30 mx-auto mb-6">
|
||||
<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="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path></svg>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold mb-2 text-center">التسجيل في مُصادَق</h2>
|
||||
<p class="text-slate-400 text-center mb-8">ابدأ تجربتك المجانية واربط مع جو-فواتير بثوانٍ</p>
|
||||
|
||||
<form id="register-form" class="space-y-4">
|
||||
<input type="text" id="reg-tenant" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="اسم الشركة (المستأجر)" required>
|
||||
<input type="text" id="reg-user" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="الاسم الكامل لمدير النظام" required>
|
||||
<input type="email" id="reg-email" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="البريد الإلكتروني" required>
|
||||
<input type="password" id="reg-password" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="كلمة المرور" required>
|
||||
<div id="register-error" class="text-red-400 text-sm text-center hidden p-2 bg-red-500/10 rounded-lg border border-red-500/20"></div>
|
||||
<button type="submit" class="w-full bg-gradient-to-r from-emerald-500 to-primary hover:from-primary hover:to-emerald-500 text-white font-bold py-3 rounded-xl shadow-lg transition-all mt-2">إنشاء حساب جديد</button>
|
||||
</form>
|
||||
<p class="mt-6 text-center text-slate-400 text-sm">لديك حساب بالفعل؟ <a href="#" onclick="renderLogin()" class="text-primary hover:underline">سجل الدخول</a></p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('register-form').onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('button');
|
||||
btn.innerHTML = 'جاري إنشاء الحساب...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const data = {
|
||||
tenant_name: document.getElementById('reg-tenant').value,
|
||||
user_name: document.getElementById('reg-user').value,
|
||||
email: document.getElementById('reg-email').value,
|
||||
password: document.getElementById('reg-password').value
|
||||
};
|
||||
const res = await API.post('/auth/register', data);
|
||||
localStorage.setItem('access_token', res.data.access_token);
|
||||
localStorage.setItem('user_role', res.data.user.role);
|
||||
API.accessToken = res.data.access_token;
|
||||
initApp();
|
||||
} catch (err) {
|
||||
const errEl = document.getElementById('register-error');
|
||||
errEl.textContent = err.error?.message_ar || err.error?.details?.message || err.message || 'خطأ في التسجيل';
|
||||
errEl.classList.remove('hidden');
|
||||
btn.innerHTML = 'إنشاء حساب جديد';
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── Dashboard View ───────────────────────────────────────
|
||||
async function renderDashboard() {
|
||||
document.getElementById('page-title').textContent = 'لوحة التحكم السريعة';
|
||||
@@ -400,7 +457,7 @@
|
||||
html += `<p class="text-slate-500 text-center py-8 bg-black/20 rounded-xl">لا توجد فواتير بعد</p>`;
|
||||
} else {
|
||||
stats.recent_invoices.forEach(inv => {
|
||||
const statusColor = inv.status === 'APPROVED' ? 'text-primary' : (inv.status === 'REJECTED' ? 'text-red-400' : 'text-yellow-400');
|
||||
const statusColor = inv.status === 'approved' ? 'text-primary' : (inv.status === 'rejected' ? 'text-red-400' : 'text-yellow-400');
|
||||
html += `
|
||||
<div class="flex justify-between items-center p-5 bg-black/30 rounded-2xl border border-white/5 hover:border-white/10 transition-colors">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -535,10 +592,10 @@
|
||||
html += `<tr><td colspan="4" class="p-8 text-center text-slate-500">لا توجد فواتير.</td></tr>`;
|
||||
} else {
|
||||
invoices.forEach(inv => {
|
||||
const statusColor = inv.status === 'APPROVED' ? 'text-primary' : (inv.status === 'REJECTED' ? 'text-red-400' : 'text-yellow-400');
|
||||
const statusColor = inv.status === 'approved' ? 'text-primary' : (inv.status === 'rejected' ? 'text-red-400' : 'text-yellow-400');
|
||||
html += `
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="p-4 font-mono text-xs text-slate-300">${inv.id}</td>
|
||||
<tr onclick="renderInvoiceDetail('${inv.id}')" class="hover:bg-white/5 transition-colors cursor-pointer group">
|
||||
<td class="p-4 font-mono text-xs text-slate-300 group-hover:text-primary transition-colors">${inv.id}</td>
|
||||
<td class="p-4 font-bold text-slate-200">${inv.company_id}</td>
|
||||
<td class="p-4 text-slate-400">${new Date(inv.created_at).toLocaleDateString('ar-JO')}</td>
|
||||
<td class="p-4 font-bold ${statusColor}">${inv.status}</td>
|
||||
@@ -554,6 +611,128 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function renderInvoiceDetail(id) {
|
||||
document.getElementById('page-title').textContent = 'تفاصيل الفاتورة';
|
||||
contentDiv.innerHTML = `<div class="flex items-center justify-center p-20"><div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div></div>`;
|
||||
|
||||
try {
|
||||
const res = await API.get(`/invoices/${id}`);
|
||||
const inv = res.data;
|
||||
|
||||
const statusColor = inv.status === 'approved' ? 'text-primary' : (inv.status === 'rejected' ? 'text-red-400' : 'text-yellow-400');
|
||||
|
||||
let linesHtml = '';
|
||||
if (inv.lines && inv.lines.length > 0) {
|
||||
inv.lines.forEach(line => {
|
||||
linesHtml += `
|
||||
<tr class="border-b border-white/5">
|
||||
<td class="py-3 text-slate-300">${line.description || 'بدون وصف'}</td>
|
||||
<td class="py-3 text-center">${line.quantity}</td>
|
||||
<td class="py-3 text-center">${line.unit_price}</td>
|
||||
<td class="py-3 text-left font-bold">${line.line_total}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
linesHtml = '<tr><td colspan="4" class="py-4 text-center text-slate-500">لا توجد بنود مستخرجة بعد</td></tr>';
|
||||
}
|
||||
|
||||
contentDiv.innerHTML = `
|
||||
<div class="flex flex-col lg:flex-row gap-6 h-[calc(100vh-200px)]">
|
||||
<!-- Left side: Original File View (Placeholder for now, could be PDF viewer) -->
|
||||
<div class="lg:w-1/2 glass-panel rounded-3xl overflow-hidden flex flex-col">
|
||||
<div class="p-4 bg-white/5 border-b border-white/10 flex justify-between items-center">
|
||||
<h4 class="font-bold">المستند الأصلي</h4>
|
||||
<a href="${inv.original_file_path}" target="_blank" class="text-xs text-primary hover:underline">فتح في نافذة جديدة</a>
|
||||
</div>
|
||||
<div class="flex-1 bg-black/40 flex items-center justify-center text-slate-500 italic p-10 text-center">
|
||||
${inv.original_file_path.endsWith('.pdf')
|
||||
? `<iframe src="${inv.original_file_path}" class="w-full h-full border-none"></iframe>`
|
||||
: `<img src="${inv.original_file_path}" class="max-w-full max-h-full object-contain" alt="Invoice Image">`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side: Extracted Data -->
|
||||
<div class="lg:w-1/2 flex flex-col gap-6 overflow-y-auto pr-2 custom-scrollbar">
|
||||
<div class="glass-panel p-6 rounded-3xl">
|
||||
<div class="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h4 class="text-lg font-bold mb-1">${inv.supplier_name || 'مورد غير معروف'}</h4>
|
||||
<p class="text-sm text-slate-400">رقم الفاتورة: ${inv.invoice_number || '---'}</p>
|
||||
</div>
|
||||
<span class="px-4 py-1 rounded-full text-xs font-bold bg-white/5 ${statusColor} border border-current">
|
||||
${inv.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="p-4 bg-white/5 rounded-2xl border border-white/5">
|
||||
<p class="text-xs text-slate-500 mb-1">تاريخ الفاتورة</p>
|
||||
<p class="font-bold">${inv.invoice_date || '---'}</p>
|
||||
</div>
|
||||
<div class="p-4 bg-white/5 rounded-2xl border border-white/5">
|
||||
<p class="text-xs text-slate-500 mb-1">الرقم الضريبي (المورد)</p>
|
||||
<p class="font-bold font-mono">${inv.supplier_tin || '---'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="font-bold mb-3 text-sm">بنود الفاتورة</h5>
|
||||
<table class="w-full text-sm mb-6">
|
||||
<thead class="text-slate-500 text-xs border-b border-white/10">
|
||||
<tr>
|
||||
<th class="py-2 text-right">الوصف</th>
|
||||
<th class="py-2 text-center">الكمية</th>
|
||||
<th class="py-2 text-center">السعر</th>
|
||||
<th class="py-2 text-left">المجموع</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${linesHtml}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="space-y-2 pt-4 border-t border-white/10">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-400">المجموع الفرعي</span>
|
||||
<span class="font-bold">${inv.subtotal} JOD</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-400">الضريبة</span>
|
||||
<span class="font-bold">${inv.tax_amount} JOD</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-lg pt-2 border-t border-white/5">
|
||||
<span class="font-bold">الإجمالي</span>
|
||||
<span class="font-black text-primary">${inv.grand_total} JOD</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mb-10">
|
||||
<button onclick="renderInvoices()" class="flex-1 py-3 bg-white/5 hover:bg-white/10 rounded-2xl transition font-bold border border-white/10">عودة</button>
|
||||
${inv.status === 'extracted' ? `
|
||||
<button onclick="submitToJoFotara('${inv.id}')" class="flex-[2] py-3 bg-gradient-to-r from-primary to-emerald-400 hover:shadow-primary/20 hover:shadow-xl rounded-2xl text-white font-bold transition-all">إرسال لـ جو-فواتير</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
contentDiv.innerHTML = `<div class="text-red-400 p-10">خطأ في تحميل تفاصيل الفاتورة: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitToJoFotara(id) {
|
||||
try {
|
||||
// We'll need a POST /api/v1/invoices/{id}/submit endpoint
|
||||
const res = await API.post(`/invoices/${id}/submit`, {});
|
||||
alert('تم إرسال الفاتورة للطابور بنجاح. سيتم تحديث الحالة تلقائياً.');
|
||||
renderInvoiceDetail(id);
|
||||
} catch (err) {
|
||||
alert(err.error?.message_ar || 'فشل الإرسال: تأكد من إعدادات الربط للشركة.');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Modals & Actions ─────────────────────────────────────
|
||||
function showAddCompanyModal() {
|
||||
const modals = document.getElementById('modals');
|
||||
|
||||
Reference in New Issue
Block a user