Commit 5b2dbb02 authored by DevPilot's avatar DevPilot

feat(sa): أداة تكرار الرقم القومي للاعبين + منع التكرار من أصله

المشكلة اللي اتبلّغت: نفس اللاعب متسجّل مرتين بنفس الرقم القومي — مرة
كعضو ومرة كغير عضو. اتأكدت من الداتا الحية: فعلاً فيه 7 أرقام قومية
مكررة، واحد منهم متكرر 4 مرات. شاشة تسجيل اللاعبين ما كانش فيها أي فحص
للتكرار، والـ index على العمود non-unique.

المنع:
- PlayerController::store() و ::update() بقوا يرفضوا أي رقم قومي مسجّل
  للاعب تاني، ويقولوا اسم اللاعب ورقمه عشان الموظف يفتح ملفه بدل ما
  يسجّله من جديد.

التنضيف (شاشة جديدة /sa/players/duplicates):
- بتعرض كل مجموعة مكررة وجوّاها اللاعبين، وقدّام كل نسخة عدد السجلات
  المرتبطة بيها (اشتراكات، حضور، بطاقات، مستندات...) عشان الموظف يعرف
  النسخة اللي عليها شغل فعلي.
- بتفرّق بين «نفس الاسم» و«أسماء مختلفة» وبتحذّر: مش كل تكرار هو نفس
  الشخص — أحياناً حد كتب رقم قومي غلط لشخص تاني.
- إجراءين: دمج (بينقل كل السجلات للنسخة المختارة ويأرشف الباقي)، أو
  تفريغ الرقم القومي لو اتضح إنهم أشخاص مختلفين.
- الدمج كله في transaction، ومفيش حذف نهائي — النسخ المدموجة بتتأرشف
  بملاحظة توضّح اتدمجت في مين.

ملاحظة مهمة في التنفيذ: في الداتابيز 45 عمود اسمه player_id، لكن كتير
منهم بيشاور على جدول players بتاع «شؤون اللاعبين» — جدول تاني خالص.
فالدمج بيشتغل على قايمة جداول مكتوبة بإيد ومتأكد منها، مش على اسم العمود،
عشان ما نخربش بيانات موديول تاني.

كمان: مسمّى «إيداع ورقة قبض برسم التحصيل» في قيود الأوراق التجارية اتغيّر
لـ «تسليم ورقة قبض للبنك للتحصيل» — «برسم التحصيل» مصطلح بنكي معناه
«عشان يتحصّل»، بس بتتقري كأن فيه رسم اتخصم.
parent 1031b8c0
......@@ -39,7 +39,9 @@ final class InstrumentPostingService
private const LEGS = [
// Received instruments
'instrument:deposited' => [
'label' => 'إيداع ورقة قبض برسم التحصيل',
// «برسم التحصيل» مصطلح بنكي معناه إن الشيك اتسلّم للبنك عشان يحصّله،
// مش إن فيه رسم اتخصم. اتكتبت بوضوح عشان محدش يفهمها غلط.
'label' => 'تسليم ورقة قبض للبنك للتحصيل',
'counter' => 'instrument:under_collection_account', // Dr شيكات تحت التحصيل
'line' => 'instrument:notes_receivable', // Cr أوراق قبض
'inflow' => true,
......
......@@ -180,6 +180,15 @@ class PlayerController extends Controller
if ($nationalId !== '' && strlen($nationalId) > 14) {
$errors[] = 'الرقم القومي يجب ألا يتجاوز 14 رقم';
}
// نفس الرقم القومي ما ينفعش يتسجّل لأكتر من لاعب — ده اللي كان بيخلي
// نفس الشخص يتسجّل مرتين، مرة كعضو ومرة كغير عضو.
if ($nationalId !== '') {
$dupePlayer = \App\Modules\SportsActivity\Services\PlayerDuplicateService::findByNationalId($nationalId, null);
if ($dupePlayer) {
$errors[] = 'الرقم القومي مسجّل بالفعل للاعب: ' . $dupePlayer['full_name_ar']
. ' (رقم ' . $dupePlayer['id'] . ') — افتح ملفه بدل ما تسجّله من جديد';
}
}
if ($phone !== '' && mb_strlen($phone) > 30) {
$errors[] = 'رقم الهاتف يجب ألا يتجاوز 30 حرف';
}
......@@ -447,6 +456,15 @@ class PlayerController extends Controller
if ($nationalId !== '' && strlen($nationalId) > 14) {
$errors[] = 'الرقم القومي يجب ألا يتجاوز 14 رقم';
}
// نفس الرقم القومي ما ينفعش يتسجّل لأكتر من لاعب — ده اللي كان بيخلي
// نفس الشخص يتسجّل مرتين، مرة كعضو ومرة كغير عضو.
if ($nationalId !== '') {
$dupePlayer = \App\Modules\SportsActivity\Services\PlayerDuplicateService::findByNationalId($nationalId, (int) $id);
if ($dupePlayer) {
$errors[] = 'الرقم القومي مسجّل بالفعل للاعب: ' . $dupePlayer['full_name_ar']
. ' (رقم ' . $dupePlayer['id'] . ') — افتح ملفه بدل ما تسجّله من جديد';
}
}
if ($phone !== '' && mb_strlen($phone) > 30) {
$errors[] = 'رقم الهاتف يجب ألا يتجاوز 30 حرف';
}
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\App;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\SportsActivity\Services\PlayerDuplicateService;
class PlayerDuplicateController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('sa.player.view');
return $this->view('SportsActivity.Views.players.duplicates', [
'groups' => PlayerDuplicateService::findDuplicates(),
]);
}
public function merge(Request $request): Response
{
$this->authorize('sa.player.manage');
$keepId = (int) $request->post('keep_id', 0);
$mergeIds = (array) ($request->post('merge_ids') ?? []);
if ($keepId <= 0) {
return $this->redirect('/sa/players/duplicates')->withError('لم يتم تحديد النسخة التي سيتم الاحتفاظ بها');
}
$employee = App::getInstance()->currentEmployee();
$result = PlayerDuplicateService::merge($keepId, $mergeIds, $employee ? (int) $employee->id : null);
if (!$result['success']) {
return $this->redirect('/sa/players/duplicates')->withError($result['error']);
}
$n = count($result['merged']);
$moved = (int) ($result['moved_total'] ?? 0);
return $this->redirect('/sa/players/duplicates')
->withSuccess("تم دمج {$n} نسخة في اللاعب رقم {$keepId} — ونُقل {$moved} سجل مرتبط");
}
public function clearNid(Request $request, string $id): Response
{
$this->authorize('sa.player.manage');
$employee = App::getInstance()->currentEmployee();
$result = PlayerDuplicateService::clearNationalId((int) $id, $employee ? (int) $employee->id : null);
if (!$result['success']) {
return $this->redirect('/sa/players/duplicates')->withError($result['error']);
}
return $this->redirect('/sa/players/duplicates')
->withSuccess('تم تفريغ الرقم القومي — افتح ملف اللاعب وأدخل الرقم الصحيح');
}
}
......@@ -47,6 +47,9 @@ return [
// Players
['GET', '/sa/players', 'SportsActivity\Controllers\PlayerController@index', ['auth'], 'sa.player.view'],
['GET', '/sa/players/duplicates', 'SportsActivity\Controllers\PlayerDuplicateController@index', ['auth'], 'sa.player.view'],
['POST', '/sa/players/duplicates/merge', 'SportsActivity\Controllers\PlayerDuplicateController@merge', ['auth', 'csrf'], 'sa.player.manage'],
['POST', '/sa/players/duplicates/{id:\d+}/clear-nid', 'SportsActivity\Controllers\PlayerDuplicateController@clearNid',['auth', 'csrf'], 'sa.player.manage'],
['GET', '/sa/players/create', 'SportsActivity\Controllers\PlayerController@create', ['auth'], 'sa.player.manage'],
['POST', '/sa/players', 'SportsActivity\Controllers\PlayerController@store', ['auth', 'csrf'], 'sa.player.manage'],
['GET', '/sa/players/{id:\d+}', 'SportsActivity\Controllers\PlayerController@show', ['auth'], 'sa.player.view'],
......
This diff is collapsed.
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تكرار الرقم القومي<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/players" class="btn btn-outline">رجوع للاعبين</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$typeLabels = ['member' => 'عضو', 'non_member' => 'غير عضو'];
$tableLabels = [
'sa_attendance' => 'حضور',
'sa_booking_participants'=> 'مشاركات حجوزات',
'sa_gate_access_log' => 'دخول البوابة',
'sa_group_players' => 'اشتراك مجموعات',
'sa_player_cards' => 'بطاقات',
'sa_player_documents' => 'مستندات',
'sa_registrations' => 'تسجيلات',
'sa_subscriptions' => 'اشتراكات',
'sa_waitlist' => 'قوائم انتظار',
'sa_game_tickets' => 'تذاكر ألعاب',
'sa_locker_rentals' => 'خزائن',
'sa_makeup_sessions' => 'حصص تعويضية',
'sa_pool_tickets' => 'تذاكر حمام سباحة',
'sa_training_attendance' => 'حضور تدريب',
];
?>
<div class="card" style="margin-bottom:14px;border-right:3px solid <?= empty($groups) ? '#059669' : '#D97706' ?>;">
<div style="padding:15px 20px;">
<h3 style="margin:0;font-size:16px;">لاعبون بنفس الرقم القومي</h3>
<p style="margin:8px 0 0;color:#6B7280;font-size:12.5px;line-height:1.9;">
شاشة تسجيل اللاعبين كانت بتقبل نفس الرقم القومي أكتر من مرة، فنفس الشخص ممكن يكون
متسجّل مرتين — مرة كعضو ومرة كغير عضو مثلاً — وكل نسخة ليها اشتراكاتها وحضورها
وبطاقتها لوحدها. ده اتقفل دلوقتي، والشاشة دي بتنضّف اللي اتسجّل قبل كده.
</p>
<p style="margin:6px 0 0;color:#92400E;font-size:12.5px;line-height:1.9;background:#FFFBEB;padding:9px 12px;border-radius:7px;">
<strong>مهم:</strong> مش كل تكرار هو نفس الشخص. أحياناً حد يكون كتب رقم قومي غلط
لشخص تاني. بُصّ على الأسماء الأول: لو نفس الشخص → <strong>ادمج</strong>،
ولو أشخاص مختلفين → <strong>فرّغ الرقم الغلط</strong> وبعدين صحّحه من ملف اللاعب.
</p>
</div>
</div>
<?php if (empty($groups)): ?>
<div class="card" style="padding:40px;text-align:center;color:#059669;">
<div style="font-size:15px;font-weight:600;">مفيش أي تكرار — كل اللاعبين بأرقام قومية مختلفة</div>
</div>
<?php else: ?>
<?php foreach ($groups as $g): ?>
<div class="card" style="margin-bottom:16px;">
<div style="padding:13px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<div>
<span style="font-size:12px;color:#6B7280;">الرقم القومي:</span>
<code style="direction:ltr;display:inline-block;font-size:13px;font-weight:700;"><?= e($g['national_id']) ?></code>
<span style="margin-inline-start:10px;background:#FEF3C7;color:#92400E;padding:2px 10px;border-radius:20px;font-size:11.5px;font-weight:700;">
<?= (int) $g['count'] ?> نسخ
</span>
</div>
<span style="font-size:12px;<?= $g['same_name'] ? 'color:#059669;' : 'color:#DC2626;font-weight:600;' ?>">
<?= $g['same_name'] ? 'نفس الاسم — على الأرجح نفس الشخص' : 'أسماء مختلفة — راجع كويس قبل الدمج' ?>
</span>
</div>
<form method="POST" action="/sa/players/duplicates/merge">
<?= csrf_field() ?>
<div class="table-responsive">
<table class="data-table" style="width:100%;font-size:12.5px;">
<thead>
<tr>
<th style="width:90px;">احتفظ به</th>
<th style="width:70px;">ادمجه</th>
<th>الاسم</th>
<th>النوع</th>
<th>مسلسل</th>
<th>تاريخ التسجيل</th>
<th>السجلات المرتبطة</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($g['players'] as $i => $p): ?>
<tr>
<td style="text-align:center;">
<input type="radio" name="keep_id" value="<?= (int) $p['id'] ?>"
<?= $i === 0 ? 'checked' : '' ?> required>
</td>
<td style="text-align:center;">
<input type="checkbox" name="merge_ids[]" value="<?= (int) $p['id'] ?>"
class="dup-merge" data-id="<?= (int) $p['id'] ?>">
</td>
<td>
<a href="/sa/players/<?= (int) $p['id'] ?>" style="font-weight:600;"><?= e($p['full_name_ar'] ?? '—') ?></a>
<span style="color:#9CA3AF;font-size:11px;">#<?= (int) $p['id'] ?></span>
</td>
<td><?= e($typeLabels[$p['player_type'] ?? ''] ?? ($p['player_type'] ?? '—')) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($p['registration_serial'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;font-size:11.5px;"><?= e(substr((string) ($p['created_at'] ?? ''), 0, 10) ?: '—') ?></td>
<td>
<?php if (($p['links']['total'] ?? 0) === 0): ?>
<span style="color:#9CA3AF;">لا شيء</span>
<?php else: ?>
<strong style="color:#0D7377;"><?= (int) $p['links']['total'] ?></strong>
<span style="color:#6B7280;font-size:11px;">
(<?php
$parts = [];
foreach ($p['links']['by_table'] as $t => $n) {
$parts[] = ($tableLabels[$t] ?? $t) . ' ' . $n;
}
echo e(implode('، ', $parts));
?>)
</span>
<?php endif; ?>
</td>
<td style="white-space:nowrap;">
<?php if (can('sa.player.manage')): ?>
<button type="submit" formaction="/sa/players/duplicates/<?= (int) $p['id'] ?>/clear-nid"
class="btn btn-sm btn-outline" style="font-size:11px;"
onclick="return confirm('تفريغ الرقم القومي لـ <?= e($p['full_name_ar'] ?? '') ?>؟ هتحتاج تدخل الرقم الصحيح بعدين من ملف اللاعب.');">
شخص مختلف
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (can('sa.player.manage')): ?>
<div style="padding:13px 20px;border-top:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;">
<span style="font-size:12px;color:#6B7280;">
اختر النسخة اللي هتفضل (يُفضَّل اللي عليها سجلات أكتر)، وعلّم على اللي هتتدمج فيها.
</span>
<button type="submit" class="btn btn-primary" style="font-size:12.5px;"
onclick="return confirm('تأكيد الدمج؟ كل السجلات المرتبطة هتتحوّل للنسخة المختارة، والباقي هيتأرشف.');">
دمج المحدد
</button>
</div>
<?php endif; ?>
</form>
</div>
<?php endforeach; ?>
<script>
// ما ينفعش تدمج النسخة اللي إنت مختارها للاحتفاظ في نفسها
document.querySelectorAll('form').forEach(function (form) {
function sync() {
var keep = form.querySelector('input[name="keep_id"]:checked');
var keepVal = keep ? keep.value : null;
form.querySelectorAll('.dup-merge').forEach(function (cb) {
var same = cb.getAttribute('data-id') === keepVal;
cb.disabled = same;
if (same) cb.checked = false;
cb.closest('tr').style.opacity = same ? '0.55' : '';
});
}
form.querySelectorAll('input[name="keep_id"]').forEach(function (r) {
r.addEventListener('change', sync);
});
sync();
});
</script>
<?php endif; ?>
<?php $__template->endSection(); ?>
......@@ -25,6 +25,7 @@ MenuRegistry::register('sports_activity', [
['label_ar' => 'المرافق', 'label_en' => 'Facilities', 'route' => '/sa/facilities', 'permission' => 'sa.facility.view', 'order' => 7],
['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/sa/coaches', 'permission' => 'sa.coach.view', 'order' => 8],
['label_ar' => 'اللاعبين', 'label_en' => 'Players', 'route' => '/sa/players', 'permission' => 'sa.player.view', 'order' => 9],
['label_ar' => 'تكرار الرقم القومي', 'label_en' => 'Duplicate IDs', 'route' => '/sa/players/duplicates', 'permission' => 'sa.player.view', 'order' => 10],
['label_ar' => 'تعيين اللاعبين', 'label_en' => 'Assignments', 'route' => '/sa/assignments', 'permission' => 'sa.player.assign', 'order' => 9.5],
['label_ar' => 'الكروت', 'label_en' => 'Cards', 'route' => '/sa/cards', 'permission' => 'sa.card.view', 'order' => 10],
['label_ar' => 'الأكاديميات', 'label_en' => 'Academies', 'route' => '/sa/academies', 'permission' => 'sa.academy.view', 'order' => 11],
......
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