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
{
public function handle(Request $request, callable $next): Response
{
// Only validate on state-changing methods
$method = strtoupper($request->method());
if (in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
return $next($request);
}
// Skip CSRF for API routes that use token auth
$path = $request->path();
if (str_starts_with($path, '/api/') && $request->bearerToken()) {
return $next($request);
}
// Get token from POST data or header
$token = $request->post('_csrf_token', '')
?: $request->header('X-CSRF-TOKEN')
?: '';
if (!CSRF::validate($token)) {
// If AJAX request, return JSON error
if ($request->isAjax() || $request->isJson()) {
$response = new Response();
return $response->json([
......@@ -39,7 +35,6 @@ class CSRFMiddleware implements MiddlewareInterface
], 419);
}
// For regular form submissions, show error page
http_response_code(419);
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;}'
......@@ -54,8 +49,8 @@ class CSRFMiddleware implements MiddlewareInterface
exit;
}
// Token valid — regenerate for next request
CSRF::regenerate();
// Do NOT regenerate token — keep it valid for the entire session
// This allows multiple AJAX calls and form submissions without breaking
return $next($request);
}
......
......@@ -6,6 +6,7 @@ namespace App\Modules\Members\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\Members\Services\MemberSearchService;
......@@ -16,57 +17,91 @@ class MemberApiController extends Controller
$nid = trim((string) $request->post('national_id', ''));
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;
if ($result['is_valid']) {
$dup = NationalIdParser::checkDuplicate($nid);
if ($dup) {
$duplicate = [
'id' => (int) $dup['id'],
'full_name_ar' => $dup['full_name_ar'],
'membership_number' => $dup['membership_number'],
'status' => $dup['status'],
'is_archived' => (bool) $dup['is_archived'],
];
if ($parsed['is_valid']) {
$db = App::getInstance()->db();
// Check members
try {
$dup = $db->selectOne(
"SELECT id, full_name_ar, membership_number FROM members WHERE national_id = ? AND is_archived = 0",
[$nid]
);
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([
'parsed' => $result,
'parsed' => $parsed,
'duplicate' => $duplicate,
]);
}
public function quickSearch(Request $request): Response
public function search(Request $request): Response
{
$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
{
$nid = trim((string) $request->post('national_id', ''));
$excludeId = $request->post('exclude_id', null);
if ($nid === '') {
return $this->json(['exists' => false]);
if ($query === '' || mb_strlen($query) < 2) {
return $this->json(['results' => []]);
}
$dup = NationalIdParser::checkDuplicate($nid, $excludeId ? (int) $excludeId : null);
$results = MemberSearchService::search($query, 20);
return $this->json([
'exists' => $dup !== null,
'member' => $dup,
]);
return $this->json(['results' => $results]);
}
}
\ No newline at end of file
......@@ -4,106 +4,38 @@ declare(strict_types=1);
namespace App\Modules\Members\Services;
use App\Core\App;
use App\Core\Pagination;
final class MemberSearchService
{
/**
* Search members with multiple criteria.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
public static function search(string $query, int $limit = 25): array
{
$db = App::getInstance()->db();
$where = "m.is_archived = 0";
$params = [];
$query = trim($query);
if (!empty($filters['q'])) {
$q = trim($filters['q']);
$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 ($query === '') {
return [];
}
if (!empty($filters['status'])) {
$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'];
}
$like = '%' . $query . '%';
if (!empty($filters['membership_number'])) {
$where .= " AND m.membership_number = ?";
$params[] = $filters['membership_number'];
}
$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
return $db->select(
"SELECT m.id, m.full_name_ar, m.full_name_en, m.national_id,
m.membership_number, m.phone_mobile, m.status, m.form_number,
b.name_ar as branch_name
FROM members m
LEFT JOIN branches b ON b.id = m.branch_id
LEFT JOIN qualifications q ON q.id = m.qualification_id
WHERE {$where}
ORDER BY m.created_at DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
$pagination = Pagination::paginate($total, $perPage, $page);
return ['data' => $rows, 'pagination' => $pagination];
}
/**
* 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
WHERE m.is_archived = 0
AND (
m.full_name_ar LIKE ?
OR m.national_id LIKE ?
OR m.membership_number LIKE ?
OR m.phone_mobile LIKE ?
OR m.form_number LIKE ?
OR m.full_name_en LIKE ?
)
ORDER BY m.full_name_ar
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