Commit 8479c580 authored by Administrator's avatar Administrator

Update 3 files via Son of Anton

parent c6199662
...@@ -12,25 +12,21 @@ class CSRFMiddleware implements MiddlewareInterface ...@@ -12,25 +12,21 @@ class CSRFMiddleware implements MiddlewareInterface
{ {
public function handle(Request $request, callable $next): Response public function handle(Request $request, callable $next): Response
{ {
// Only validate on state-changing methods
$method = strtoupper($request->method()); $method = strtoupper($request->method());
if (in_array($method, ['GET', 'HEAD', 'OPTIONS'])) { if (in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
return $next($request); return $next($request);
} }
// Skip CSRF for API routes that use token auth
$path = $request->path(); $path = $request->path();
if (str_starts_with($path, '/api/') && $request->bearerToken()) { if (str_starts_with($path, '/api/') && $request->bearerToken()) {
return $next($request); return $next($request);
} }
// Get token from POST data or header
$token = $request->post('_csrf_token', '') $token = $request->post('_csrf_token', '')
?: $request->header('X-CSRF-TOKEN') ?: $request->header('X-CSRF-TOKEN')
?: ''; ?: '';
if (!CSRF::validate($token)) { if (!CSRF::validate($token)) {
// If AJAX request, return JSON error
if ($request->isAjax() || $request->isJson()) { if ($request->isAjax() || $request->isJson()) {
$response = new Response(); $response = new Response();
return $response->json([ return $response->json([
...@@ -39,7 +35,6 @@ class CSRFMiddleware implements MiddlewareInterface ...@@ -39,7 +35,6 @@ class CSRFMiddleware implements MiddlewareInterface
], 419); ], 419);
} }
// For regular form submissions, show error page
http_response_code(419); http_response_code(419);
echo '<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="UTF-8"><title>419</title>' echo '<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="UTF-8"><title>419</title>'
. '<style>body{font-family:Cairo,Arial,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#F3F4F6;}' . '<style>body{font-family:Cairo,Arial,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#F3F4F6;}'
...@@ -54,8 +49,8 @@ class CSRFMiddleware implements MiddlewareInterface ...@@ -54,8 +49,8 @@ class CSRFMiddleware implements MiddlewareInterface
exit; exit;
} }
// Token valid — regenerate for next request // Do NOT regenerate token — keep it valid for the entire session
CSRF::regenerate(); // This allows multiple AJAX calls and form submissions without breaking
return $next($request); return $next($request);
} }
......
...@@ -6,6 +6,7 @@ namespace App\Modules\Members\Controllers; ...@@ -6,6 +6,7 @@ namespace App\Modules\Members\Controllers;
use App\Core\Controller; use App\Core\Controller;
use App\Core\Request; use App\Core\Request;
use App\Core\Response; use App\Core\Response;
use App\Core\App;
use App\Modules\Members\Services\NationalIdParser; use App\Modules\Members\Services\NationalIdParser;
use App\Modules\Members\Services\MemberSearchService; use App\Modules\Members\Services\MemberSearchService;
...@@ -16,57 +17,91 @@ class MemberApiController extends Controller ...@@ -16,57 +17,91 @@ class MemberApiController extends Controller
$nid = trim((string) $request->post('national_id', '')); $nid = trim((string) $request->post('national_id', ''));
if ($nid === '') { if ($nid === '') {
return $this->json(['error' => 'الرقم القومي مطلوب'], 422); return $this->json(['error' => 'الرقم القومي مطلوب', 'parsed' => ['is_valid' => false, 'errors' => ['الرقم القومي مطلوب']]]);
} }
$result = NationalIdParser::parse($nid); $parsed = NationalIdParser::parse($nid);
// Also check duplicate // Check for duplicates across all tables
$duplicate = null; $duplicate = null;
if ($result['is_valid']) { if ($parsed['is_valid']) {
$dup = NationalIdParser::checkDuplicate($nid); $db = App::getInstance()->db();
if ($dup) {
$duplicate = [ // Check members
'id' => (int) $dup['id'], try {
'full_name_ar' => $dup['full_name_ar'], $dup = $db->selectOne(
'membership_number' => $dup['membership_number'], "SELECT id, full_name_ar, membership_number FROM members WHERE national_id = ? AND is_archived = 0",
'status' => $dup['status'], [$nid]
'is_archived' => (bool) $dup['is_archived'], );
]; if ($dup) {
$duplicate = [
'type' => 'member',
'id' => (int) $dup['id'],
'full_name_ar' => $dup['full_name_ar'],
'membership_number' => $dup['membership_number'] ?? '',
];
}
} catch (\Throwable $e) {}
// Check spouses
if (!$duplicate) {
try {
$dup = $db->selectOne(
"SELECT s.id, s.full_name_ar, s.member_id, m.membership_number
FROM spouses s JOIN members m ON m.id = s.member_id
WHERE s.national_id = ? AND s.is_archived = 0",
[$nid]
);
if ($dup) {
$duplicate = [
'type' => 'spouse',
'id' => (int) $dup['id'],
'full_name_ar' => $dup['full_name_ar'],
'membership_number' => $dup['membership_number'] ?? '',
'member_id' => (int) $dup['member_id'],
];
}
} catch (\Throwable $e) {}
}
// Check children
if (!$duplicate) {
try {
$dup = $db->selectOne(
"SELECT c.id, c.full_name_ar, c.member_id, m.membership_number
FROM children c JOIN members m ON m.id = c.member_id
WHERE c.national_id = ? AND c.is_archived = 0",
[$nid]
);
if ($dup) {
$duplicate = [
'type' => 'child',
'id' => (int) $dup['id'],
'full_name_ar' => $dup['full_name_ar'],
'membership_number' => $dup['membership_number'] ?? '',
'member_id' => (int) $dup['member_id'],
];
}
} catch (\Throwable $e) {}
} }
} }
return $this->json([ return $this->json([
'parsed' => $result, 'parsed' => $parsed,
'duplicate' => $duplicate, 'duplicate' => $duplicate,
]); ]);
} }
public function quickSearch(Request $request): Response public function search(Request $request): Response
{ {
$query = trim((string) $request->post('q', '')); $query = trim((string) $request->post('q', ''));
if (mb_strlen($query) < 2) {
return $this->json(['results' => []]);
}
$results = MemberSearchService::quickSearch($query, 10);
return $this->json(['results' => $results]);
}
public function checkDuplicate(Request $request): Response if ($query === '' || mb_strlen($query) < 2) {
{ return $this->json(['results' => []]);
$nid = trim((string) $request->post('national_id', ''));
$excludeId = $request->post('exclude_id', null);
if ($nid === '') {
return $this->json(['exists' => false]);
} }
$dup = NationalIdParser::checkDuplicate($nid, $excludeId ? (int) $excludeId : null); $results = MemberSearchService::search($query, 20);
return $this->json([ return $this->json(['results' => $results]);
'exists' => $dup !== null,
'member' => $dup,
]);
} }
} }
\ No newline at end of file
...@@ -4,106 +4,38 @@ declare(strict_types=1); ...@@ -4,106 +4,38 @@ declare(strict_types=1);
namespace App\Modules\Members\Services; namespace App\Modules\Members\Services;
use App\Core\App; use App\Core\App;
use App\Core\Pagination;
final class MemberSearchService final class MemberSearchService
{ {
/** public static function search(string $query, int $limit = 25): array
* Search members with multiple criteria.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$where = "m.is_archived = 0"; $query = trim($query);
$params = [];
if (!empty($filters['q'])) { if ($query === '') {
$q = trim($filters['q']); return [];
$where .= " AND (m.full_name_ar LIKE ? OR m.full_name_en LIKE ? OR m.national_id LIKE ? OR m.membership_number LIKE ? OR m.phone_mobile LIKE ? OR m.form_number LIKE ? OR m.passport_number LIKE ?)";
$s = "%{$q}%";
$params = array_merge($params, [$s, $s, $s, $s, $s, $s, $s]);
} }
if (!empty($filters['status'])) { $like = '%' . $query . '%';
$where .= " AND m.status = ?";
$params[] = $filters['status'];
}
if (!empty($filters['branch_id'])) {
$where .= " AND m.branch_id = ?";
$params[] = (int) $filters['branch_id'];
}
if (!empty($filters['membership_type'])) {
$where .= " AND m.membership_type = ?";
$params[] = $filters['membership_type'];
}
if (!empty($filters['gender'])) {
$where .= " AND m.gender = ?";
$params[] = $filters['gender'];
}
if (!empty($filters['date_from'])) {
$where .= " AND m.created_at >= ?";
$params[] = $filters['date_from'] . ' 00:00:00';
}
if (!empty($filters['date_to'])) {
$where .= " AND m.created_at <= ?";
$params[] = $filters['date_to'] . ' 23:59:59';
}
if (!empty($filters['national_id'])) {
$where .= " AND m.national_id = ?";
$params[] = $filters['national_id'];
}
if (!empty($filters['membership_number'])) { return $db->select(
$where .= " AND m.membership_number = ?"; "SELECT m.id, m.full_name_ar, m.full_name_en, m.national_id,
$params[] = $filters['membership_number']; m.membership_number, m.phone_mobile, m.status, m.form_number,
} b.name_ar as branch_name
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM members m WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT m.*, b.name_ar as branch_name, q.name_ar as qualification_name
FROM members m FROM members m
LEFT JOIN branches b ON b.id = m.branch_id LEFT JOIN branches b ON b.id = m.branch_id
LEFT JOIN qualifications q ON q.id = m.qualification_id WHERE m.is_archived = 0
WHERE {$where} AND (
ORDER BY m.created_at DESC m.full_name_ar LIKE ?
LIMIT {$perPage} OFFSET {$offset}", OR m.national_id LIKE ?
$params OR m.membership_number LIKE ?
); OR m.phone_mobile LIKE ?
OR m.form_number LIKE ?
$pagination = Pagination::paginate($total, $perPage, $page); OR m.full_name_en LIKE ?
)
return ['data' => $rows, 'pagination' => $pagination]; ORDER BY m.full_name_ar
}
/**
* Quick search for AJAX autocomplete (header search bar).
*/
public static function quickSearch(string $query, int $limit = 10): array
{
$db = App::getInstance()->db();
$s = "%{$query}%";
return $db->select(
"SELECT id, membership_number, full_name_ar, national_id, phone_mobile, status
FROM members
WHERE is_archived = 0
AND (full_name_ar LIKE ? OR membership_number LIKE ? OR national_id LIKE ? OR phone_mobile LIKE ?)
ORDER BY
CASE WHEN membership_number = ? THEN 0
WHEN national_id = ? THEN 1
ELSE 2 END,
full_name_ar ASC
LIMIT ?", LIMIT ?",
[$s, $s, $s, $s, $query, $query, $limit] [$like, $like, $like, $like, $like, $like, $limit]
); );
} }
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment