Update: 2026-05-08 01:41:28
This commit is contained in:
124
app/modules_app/audit/index.php
Normal file
124
app/modules_app/audit/index.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* Audit Log / Activity History
|
||||
* GET /v1/audit-log
|
||||
* Returns paginated activity history
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$role = $decoded['role'];
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$limit = min(50, max(10, (int)($_GET['limit'] ?? 20)));
|
||||
$offset = ($page - 1) * $limit;
|
||||
$entityType = $_GET['entity_type'] ?? null;
|
||||
$action = $_GET['action'] ?? null;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($role !== 'super_admin') {
|
||||
$where[] = 'a.tenant_id = ?';
|
||||
$params[] = $tenantId;
|
||||
}
|
||||
|
||||
if ($entityType) {
|
||||
$where[] = 'a.entity_type = ?';
|
||||
$params[] = $entityType;
|
||||
}
|
||||
|
||||
if ($action) {
|
||||
$where[] = 'a.action LIKE ?';
|
||||
$params[] = "%$action%";
|
||||
}
|
||||
|
||||
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
// Total count
|
||||
$countStmt = $db->prepare("SELECT COUNT(*) FROM audit_log a $whereClause");
|
||||
$countStmt->execute($params);
|
||||
$total = $countStmt->fetchColumn();
|
||||
|
||||
// Fetch logs
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT a.*, u.name as user_name
|
||||
FROM audit_log a
|
||||
LEFT JOIN users u ON a.user_id = u.id
|
||||
$whereClause
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$logs = $stmt->fetchAll();
|
||||
|
||||
// Format logs
|
||||
foreach ($logs as &$log) {
|
||||
$log['details'] = json_decode($log['details'] ?? '{}', true);
|
||||
$log['old_values'] = json_decode($log['old_values'] ?? '{}', true);
|
||||
|
||||
// Generate human-readable summary
|
||||
$log['summary'] = match(true) {
|
||||
str_starts_with($log['action'], 'invoice.') => _invoiceSummary($log),
|
||||
str_starts_with($log['action'], 'user.') => _userSummary($log),
|
||||
str_starts_with($log['action'], 'company.') => _companySummary($log),
|
||||
str_starts_with($log['action'], 'payment.') => _paymentSummary($log),
|
||||
default => $log['action'],
|
||||
};
|
||||
}
|
||||
unset($log);
|
||||
|
||||
json_success([
|
||||
'logs' => $logs,
|
||||
'pagination' => [
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'total' => (int)$total,
|
||||
'pages' => ceil($total / $limit),
|
||||
],
|
||||
]);
|
||||
|
||||
function _invoiceSummary(array $log): string {
|
||||
return match($log['action']) {
|
||||
'invoice.approved' => 'تم اعتماد فاتورة',
|
||||
'invoice.updated' => 'تم تعديل فاتورة',
|
||||
'invoice.bulk_approved' => 'اعتماد جماعي',
|
||||
'invoice.uploaded' => 'تم رفع فاتورة',
|
||||
'invoice.extracted' => 'تم استخراج بيانات فاتورة',
|
||||
default => $log['action'],
|
||||
};
|
||||
}
|
||||
|
||||
function _userSummary(array $log): string {
|
||||
return match($log['action']) {
|
||||
'user.created' => 'تم إنشاء مستخدم جديد',
|
||||
'user.updated' => 'تم تعديل بيانات مستخدم',
|
||||
'user.deleted' => 'تم حذف مستخدم',
|
||||
'user.login' => 'تسجيل دخول',
|
||||
default => $log['action'],
|
||||
};
|
||||
}
|
||||
|
||||
function _companySummary(array $log): string {
|
||||
return match($log['action']) {
|
||||
'company.created' => 'تم إنشاء شركة جديدة',
|
||||
'company.updated' => 'تم تعديل بيانات شركة',
|
||||
default => $log['action'],
|
||||
};
|
||||
}
|
||||
|
||||
function _paymentSummary(array $log): string {
|
||||
return match($log['action']) {
|
||||
'payment.created' => 'تم إنشاء طلب دفع',
|
||||
'payment.uploaded' => 'تم رفع وصل دفع',
|
||||
'payment.approved' => 'تم اعتماد دفعة',
|
||||
default => $log['action'],
|
||||
};
|
||||
}
|
||||
130
app/modules_app/invoices/check_duplicate.php
Normal file
130
app/modules_app/invoices/check_duplicate.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* Check Duplicate Invoices
|
||||
* POST /v1/invoices/check-duplicate
|
||||
* Checks if similar invoice exists before processing
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$data = input();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$invoiceNumber = $data['invoice_number'] ?? null;
|
||||
$supplierTin = $data['supplier_tin'] ?? null;
|
||||
$grandTotal = $data['grand_total'] ?? null;
|
||||
$invoiceDate = $data['invoice_date'] ?? null;
|
||||
$excludeId = $data['exclude_id'] ?? null;
|
||||
|
||||
$duplicates = [];
|
||||
|
||||
// 1. Exact match on invoice number
|
||||
if ($invoiceNumber) {
|
||||
$sql = "SELECT id, invoice_number, invoice_date, grand_total, status, supplier_name
|
||||
FROM invoices WHERE invoice_number = ? AND tenant_id = ?";
|
||||
$params = [$invoiceNumber, $tenantId];
|
||||
|
||||
if ($excludeId) {
|
||||
$sql .= " AND id != ?";
|
||||
$params[] = $excludeId;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$matches = $stmt->fetchAll();
|
||||
|
||||
foreach ($matches as $m) {
|
||||
$decName = Encryption::decrypt($m['supplier_name']);
|
||||
$duplicates[] = [
|
||||
'id' => $m['id'],
|
||||
'invoice_number' => $m['invoice_number'],
|
||||
'invoice_date' => $m['invoice_date'],
|
||||
'grand_total' => $m['grand_total'],
|
||||
'status' => $m['status'],
|
||||
'supplier_name' => ($decName !== false && $decName !== null) ? $decName : $m['supplier_name'],
|
||||
'match_type' => 'exact_number',
|
||||
'confidence' => 100,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fuzzy match: same supplier TIN + same total + same date
|
||||
if ($supplierTin && $grandTotal && $invoiceDate && empty($duplicates)) {
|
||||
$sql = "SELECT id, invoice_number, invoice_date, grand_total, status, supplier_name, supplier_tin
|
||||
FROM invoices
|
||||
WHERE tenant_id = ?
|
||||
AND invoice_date = ?
|
||||
AND ABS(grand_total - ?) < 0.01";
|
||||
$params = [$tenantId, $invoiceDate, $grandTotal];
|
||||
|
||||
if ($excludeId) {
|
||||
$sql .= " AND id != ?";
|
||||
$params[] = $excludeId;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$matches = $stmt->fetchAll();
|
||||
|
||||
foreach ($matches as $m) {
|
||||
$decTin = Encryption::decrypt($m['supplier_tin']);
|
||||
$decName = Encryption::decrypt($m['supplier_name']);
|
||||
|
||||
if ($decTin === $supplierTin || $m['supplier_tin'] === $supplierTin) {
|
||||
$duplicates[] = [
|
||||
'id' => $m['id'],
|
||||
'invoice_number' => $m['invoice_number'],
|
||||
'invoice_date' => $m['invoice_date'],
|
||||
'grand_total' => $m['grand_total'],
|
||||
'status' => $m['status'],
|
||||
'supplier_name' => ($decName !== false && $decName !== null) ? $decName : $m['supplier_name'],
|
||||
'match_type' => 'fuzzy_tin_total_date',
|
||||
'confidence' => 90,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Near match: same total + near date (±3 days)
|
||||
if ($grandTotal && $invoiceDate && empty($duplicates)) {
|
||||
$sql = "SELECT id, invoice_number, invoice_date, grand_total, status, supplier_name
|
||||
FROM invoices
|
||||
WHERE tenant_id = ?
|
||||
AND ABS(grand_total - ?) < 0.01
|
||||
AND ABS(DATEDIFF(invoice_date, ?)) <= 3";
|
||||
$params = [$tenantId, $grandTotal, $invoiceDate];
|
||||
|
||||
if ($excludeId) {
|
||||
$sql .= " AND id != ?";
|
||||
$params[] = $excludeId;
|
||||
}
|
||||
$sql .= " LIMIT 5";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$matches = $stmt->fetchAll();
|
||||
|
||||
foreach ($matches as $m) {
|
||||
$decName = Encryption::decrypt($m['supplier_name']);
|
||||
$duplicates[] = [
|
||||
'id' => $m['id'],
|
||||
'invoice_number' => $m['invoice_number'],
|
||||
'invoice_date' => $m['invoice_date'],
|
||||
'grand_total' => $m['grand_total'],
|
||||
'status' => $m['status'],
|
||||
'supplier_name' => ($decName !== false && $decName !== null) ? $decName : $m['supplier_name'],
|
||||
'match_type' => 'near_total_date',
|
||||
'confidence' => 60,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
json_success([
|
||||
'is_duplicate' => !empty($duplicates),
|
||||
'matches' => $duplicates,
|
||||
'count' => count($duplicates),
|
||||
], empty($duplicates) ? 'لا توجد فواتير مكررة' : 'تم العثور على فواتير مشابهة');
|
||||
42
app/modules_app/notifications/index.php
Normal file
42
app/modules_app/notifications/index.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* In-App Notifications
|
||||
* GET /v1/notifications
|
||||
* Returns user's notifications
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$userId = $decoded['user_id'];
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$limit = min(50, max(10, (int)($_GET['limit'] ?? 20)));
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// Get total + unread count
|
||||
$countStmt = $db->prepare("SELECT COUNT(*) as total, SUM(CASE WHEN is_read = 0 THEN 1 ELSE 0 END) as unread FROM notifications WHERE user_id = ?");
|
||||
$countStmt->execute([$userId]);
|
||||
$counts = $countStmt->fetch();
|
||||
|
||||
// Fetch notifications
|
||||
$stmt = $db->prepare("
|
||||
SELECT * FROM notifications
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
");
|
||||
$stmt->execute([$userId, $limit, $offset]);
|
||||
$notifications = $stmt->fetchAll();
|
||||
|
||||
json_success([
|
||||
'notifications' => $notifications,
|
||||
'unread_count' => (int)($counts['unread'] ?? 0),
|
||||
'pagination' => [
|
||||
'page' => $page,
|
||||
'total' => (int)($counts['total'] ?? 0),
|
||||
'pages' => ceil(($counts['total'] ?? 0) / $limit),
|
||||
],
|
||||
]);
|
||||
28
app/modules_app/notifications/read.php
Normal file
28
app/modules_app/notifications/read.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Mark Notification(s) as Read
|
||||
* POST /v1/notifications/read
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$data = input();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$userId = $decoded['user_id'];
|
||||
$id = $data['id'] ?? null;
|
||||
$markAll = $data['mark_all'] ?? false;
|
||||
|
||||
if ($markAll) {
|
||||
$db->prepare("UPDATE notifications SET is_read = 1, read_at = NOW() WHERE user_id = ? AND is_read = 0")
|
||||
->execute([$userId]);
|
||||
json_success(null, 'تم تعليم جميع الإشعارات كمقروءة');
|
||||
} elseif ($id) {
|
||||
$db->prepare("UPDATE notifications SET is_read = 1, read_at = NOW() WHERE id = ? AND user_id = ?")
|
||||
->execute([$id, $userId]);
|
||||
json_success(null, 'تم تعليم الإشعار كمقروء');
|
||||
} else {
|
||||
json_error('يرجى تحديد الإشعار', 422);
|
||||
}
|
||||
Reference in New Issue
Block a user