country + status added and other changes

This commit is contained in:
2026-06-10 18:02:17 +05:45
parent 5a085148b4
commit a551ca538e
16 changed files with 1386 additions and 293 deletions
+394 -37
View File
@@ -9,71 +9,428 @@
<h1 class="text-slate-900 text-xl font-bold">All Registrations</h1>
<p class="text-slate-500 text-sm mt-0.5">Complete list of registered users</p>
</div>
<div class="flex items-center gap-2.5">
<a href="{{ route('home') }}"
class="flex items-center gap-2 bg-white hover:bg-slate-50 border border-slate-200 text-slate-600 text-sm font-medium px-4 py-2 rounded-lg transition-colors shadow-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>
</svg>
Dashboard
</a>
<form method="GET" action="{{ route('registrations.index') }}" class="flex items-center">
<div class="flex items-center gap-2 bg-white border border-slate-200 rounded-lg px-3 py-2 shadow-sm focus-within:ring-2 focus-within:ring-indigo-400 transition-all">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input type="text" name="search" value="{{ $search ?? '' }}" placeholder="Search name, phone, email…"
class="text-sm text-slate-700 placeholder-slate-400 outline-none bg-transparent w-52">
@if(!empty($search))
<a href="{{ route('registrations.index') }}" class="text-slate-400 hover:text-slate-600 transition-colors">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</a>
@endif
</div>
</form>
</div>
</div>
<!-- TABLE -->
<div class="bg-white border rounded-xl overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 border-b">
<tr>
<th class="px-5 py-3 text-left">ID</th>
<th class="px-5 py-3 text-left">Name</th>
<th class="px-5 py-3 text-left">Phone</th>
<th class="px-5 py-3 text-left">Email</th>
<th class="px-5 py-3 text-center">Total Score</th>
<th class="px-5 py-3 text-center">Created</th>
<th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider w-16">ID</th>
<th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Name</th>
<th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Phone</th>
<th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Email</th>
<th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Country</th>
<th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
<th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Score</th>
<th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Sessions</th>
<th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Comments</th>
<th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Registered</th>
</tr>
</thead>
<tbody>
<tbody class="divide-y divide-slate-100">
@foreach ($registrations as $reg)
<tr class="border-b hover:bg-slate-50">
@php
$sessionData = $reg->sessions->sortByDesc('play_date')->map(fn($s) => [
'date' => $s->play_date,
'score' => $s->score,
'shots' => $s->shots->sortBy('shot_number')->map(fn($sh) => (bool)$sh->result)->values()->toArray(),
])->values()->toArray();
<td class="px-5 py-3">#{{ $reg->id }}</td>
<td class="px-5 py-3 font-medium">
{{ $reg->name ?? '-' }}
$commentData = $reg->comments->sortByDesc('created_at')->map(fn($c) => [
'comment' => $c->comment,
'created_at' => $c->created_at->diffForHumans(),
])->values()->toArray();
@endphp
<tr class="hover:bg-slate-50/70 transition-colors group cursor-pointer"
data-reg-id="{{ $reg->id }}"
data-name="{{ $reg->name }}"
data-phone="{{ $reg->phone }}"
data-email="{{ $reg->email ?? '' }}"
data-score="{{ $reg->total_score }}"
data-created="{{ $reg->created_at->format('Y-m-d') }}"
data-sessions="{{ json_encode($sessionData) }}"
data-country-title="{{ $reg->country?->title ?? '' }}"
data-country-flag="{{ $reg->country?->country_flag ?? '' }}"
data-status="{{ $reg->status ?? 'warm' }}"
data-comments="{{ json_encode($commentData) }}">
<td class="px-5 py-3.5">
<span class="text-xs font-mono text-slate-400">#{{ str_pad($reg->id, 4, '0', STR_PAD_LEFT) }}</span>
</td>
<td class="px-5 py-3">
{{ $reg->phone }}
<td class="px-5 py-3.5">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center shrink-0">
<span class="text-xs font-bold text-indigo-600">{{ strtoupper(substr($reg->name, 0, 1)) }}</span>
</div>
<span class="font-medium text-slate-800">{{ $reg->name ?? '-' }}</span>
</div>
</td>
<td class="px-5 py-3">
{{ $reg->email ?? '-' }}
<td class="px-5 py-3.5 text-slate-500">{{ $reg->phone }}</td>
<td class="px-5 py-3.5 text-slate-500">{{ $reg->email ?? '-' }}</td>
{{-- Country TD (editable with flag preview) --}}
<td class="px-5 py-3.5" onclick="event.stopPropagation()">
<div class="flex items-center gap-2">
<img id="flag-{{ $reg->id }}"
src="{{ $reg->country?->country_flag ?? '' }}"
class="w-5 h-3.5 object-cover rounded-sm {{ $reg->country_id ? '' : 'hidden' }}"
alt="">
<select class="country-select text-xs border border-slate-200 rounded-lg px-2 py-1.5 bg-white
text-slate-700 focus:outline-none focus:ring-2 focus:ring-indigo-400 cursor-pointer max-w-[140px]"
data-reg-id="{{ $reg->id }}"
data-flag-target="flag-{{ $reg->id }}">
<option value=""> Select </option>
@foreach($countries as $country)
<option value="{{ $country->id }}"
data-flag="{{ $country->country_flag }}"
{{ $reg->country_id == $country->id ? 'selected' : '' }}>
{{ $country->title }}
</option>
@endforeach
</select>
</div>
</td>
<td class="px-5 py-3 text-center font-bold">
{{ $reg->total_score }}
{{-- Status TD (read-only badge) --}}
<td class="px-5 py-3.5 text-center" onclick="event.stopPropagation()">
@php $status = $reg->status ?? 'warm'; @endphp
<span class="text-xs font-semibold rounded-full px-2.5 py-1
{{ match($status) {
'hot' => 'bg-red-50 text-red-600',
'warm' => 'bg-amber-50 text-amber-600',
'cold' => 'bg-blue-50 text-blue-600',
default => 'bg-amber-50 text-amber-600',
} }}">
{{ match($status) { 'hot' => '🔥 Hot', 'warm' => '☀️ Warm', 'cold' => '❄️ Cold', default => '☀️ Warm' } }}
</span>
</td>
<td class="px-5 py-3 text-center text-slate-500">
{{ $reg->created_at->format('Y-m-d') }}
<td class="px-5 py-3.5 text-center">
<div class="inline-flex items-center gap-1 font-bold text-slate-900">
<svg width="13" height="13" viewBox="0 0 24 24" fill="#6366f1" stroke="none"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<span class="text-base">{{ $reg->total_score }}</span>
</div>
</td>
<td class="px-5 py-3.5 text-center">
<span class="inline-flex items-center gap-1 text-xs font-medium text-slate-500 bg-slate-100 px-2 py-1 rounded-full">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 000 20M12 2a14.5 14.5 0 010 20M2 12h20"/></svg>
{{ $reg->sessions->count() }}
</span>
</td>
<td class="px-5 py-3.5 text-center">
<span class="inline-flex items-center gap-1 text-xs font-medium text-slate-500 bg-slate-100 px-2 py-1 rounded-full">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
{{ $reg->comments->count() }}
</span>
</td>
<td class="px-5 py-3.5 text-center text-slate-500 text-xs">{{ $reg->created_at->format('Y-m-d') }}</td>
</tr>
@endforeach
</tbody>
</table>
<!-- PAGINATION -->
<div class="px-5 py-3 border-t flex justify-between items-center">
<div class="text-xs text-slate-500">
Showing {{ $registrations->firstItem() }} - {{ $registrations->lastItem() }}
Showing {{ $registrations->firstItem() }}{{ $registrations->lastItem() }}
of {{ $registrations->total() }}
</div>
<div>{{ $registrations->appends(request()->query())->links() }}</div>
</div>
</div>
</div>
<div>
{{ $registrations->links() }}
<!-- STUDENT DETAIL SIDE PANEL -->
<div id="detailPanel"
class="fixed top-0 right-0 h-full w-[32rem] bg-white border-l border-slate-200 shadow-2xl z-40
transform translate-x-full transition-transform duration-300 ease-in-out flex flex-col">
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50 flex items-center justify-between shrink-0">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center shrink-0">
<span id="panelInitial" class="text-sm font-bold text-indigo-600"></span>
</div>
<div>
<p id="panelName" class="text-slate-900 font-bold text-base leading-tight"></p>
<p id="panelPhone" class="text-slate-400 text-xs mt-0.5"></p>
<div class="flex items-center gap-2 mt-1">
<img id="panelCountryFlag" class="w-5 h-3.5 rounded-sm hidden">
<span id="panelCountryTitle" class="text-xs text-slate-500"></span>
<span id="panelStatus" class="text-xs px-2 py-0.5 rounded-full"></span>
</div>
</div>
</div>
<button id="closeDetailPanel"
class="w-7 h-7 rounded-full bg-slate-200 hover:bg-slate-300 flex items-center justify-center text-slate-500 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="px-6 py-4 border-b border-slate-100 shrink-0">
<div class="grid grid-cols-3 gap-3">
<div class="bg-slate-50 rounded-xl px-3 py-3 text-center border border-slate-100">
<p class="text-xs text-slate-400 font-medium mb-1">Total Score</p>
<div class="flex items-center justify-center gap-1">
<svg width="13" height="13" viewBox="0 0 24 24" fill="#6366f1" stroke="none"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<span id="panelScore" class="text-xl font-black text-slate-900">0</span>
</div>
</div>
<div class="bg-slate-50 rounded-xl px-3 py-3 text-center border border-slate-100">
<p class="text-xs text-slate-400 font-medium mb-1">Sessions</p>
<p id="panelSessions" class="text-xl font-black text-slate-900">0</p>
</div>
<div class="bg-slate-50 rounded-xl px-3 py-3 text-center border border-slate-100">
<p class="text-xs text-slate-400 font-medium mb-1">Email</p>
<p id="panelEmail" class="text-xs font-medium text-slate-600 truncate"></p>
</div>
</div>
<p class="text-xs text-slate-400 mt-3">Registered: <span id="panelCreated" class="text-slate-500 font-medium"></span></p>
</div>
<div class="flex-1 overflow-y-auto min-h-0">
<div class="px-6 py-4 border-b border-slate-100">
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Score History</p>
<div id="panelHistory" class="space-y-2">
<p class="text-xs text-slate-400 italic">No sessions yet.</p>
</div>
</div>
<div class="px-6 py-4">
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Comments</p>
<div id="panelComments" class="space-y-2">
<p class="text-xs text-slate-400 italic">No comments yet.</p>
</div>
<div class="mt-4">
<textarea id="panelCommentText" rows="3" placeholder="Write a comment…"
class="w-full border border-slate-200 rounded-xl px-3 py-2.5 text-sm text-slate-800 bg-slate-50
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500
transition-all placeholder-slate-400 resize-none"></textarea>
<button id="panelCommentBtn"
class="mt-2 w-full bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold py-2.5 rounded-xl transition-colors flex items-center justify-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
Post Comment
</button>
</div>
</div>
</div>
</div>
@endsection
<div id="detailPanelOverlay" class="fixed inset-0 bg-black/20 z-30 hidden"></div>
@endsection
@push('js')
<script>
let selectedRegId = null;
let selectedRegRow = null;
function openDetailPanel(row) {
if (selectedRegRow) selectedRegRow.classList.remove('ring-2', 'ring-inset', 'ring-indigo-400', 'bg-indigo-50/40');
selectedRegRow = row;
row.classList.add('ring-2', 'ring-inset', 'ring-indigo-400', 'bg-indigo-50/40');
selectedRegId = row.dataset.regId;
const name = row.dataset.name;
const phone = row.dataset.phone;
const email = row.dataset.email;
const score = row.dataset.score;
const created = row.dataset.created;
const sessions = JSON.parse(row.dataset.sessions || '[]');
const comments = JSON.parse(row.dataset.comments || '[]');
const countryTitle=row.dataset.countryTitle;
const countryFlag=row.dataset.countryFlag;
document.getElementById('panelInitial').textContent = name.charAt(0).toUpperCase();
document.getElementById('panelName').textContent = name;
document.getElementById('panelPhone').textContent = phone;
document.getElementById('panelScore').textContent = score;
document.getElementById('panelSessions').textContent = sessions.length;
document.getElementById('panelEmail').textContent = email || '—';
document.getElementById('panelCreated').textContent = created;
// History
const historyEl = document.getElementById('panelHistory');
if (!Array.isArray(sessions) || sessions.length === 0) {
historyEl.innerHTML = '<p class="text-xs text-slate-400 italic">No sessions yet.</p>';
} else {
historyEl.innerHTML = sessions.map(s => {
const noShots = !s.shots || s.shots.length === 0;
const shots = noShots
? `<span class="text-xs text-slate-400 italic">No shots recorded</span>`
: [0, 1, 2].map(i => {
if (s.shots[i] === undefined) return `<span class="w-5 h-5 rounded-full border-2 border-dashed border-slate-300 inline-block"></span>`;
return s.shots[i]
? `<span class="text-sm" style="filter:drop-shadow(0 0 3px rgba(0,200,50,.6))">⚽</span>`
: `<span class="text-red-400 text-sm font-bold">✕</span>`;
}).join('');
return `<div class="flex items-center justify-between bg-slate-50 border border-slate-100 rounded-xl px-3 py-2.5">
<span class="text-xs font-medium text-slate-500">${escHtml(s.date)}</span>
<div class="flex items-center gap-1">${shots}</div>
<div class="flex items-center gap-1">
<svg width="11" height="11" viewBox="0 0 24 24" fill="#6366f1" stroke="none"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<span class="text-sm font-bold text-slate-800">${s.score}</span>
<span class="text-xs text-slate-400">pts</span>
</div>
</div>`;
}).join('');
}
// Comments — already plain arrays from toArray()
renderPanelComments(Array.isArray(comments) ? comments : Object.values(comments));
document.getElementById('panelCommentText').value = '';
//country
const flag=document.getElementById('panelCountryFlag');
if(countryFlag){
flag.src=countryFlag;
flag.classList.remove('hidden');
}else flag.classList.add('hidden');
document.getElementById('panelCountryTitle').textContent=countryTitle||'—';
// status
const statusEl=document.getElementById('panelStatus');
let cls='',txt='';
if(status==='hot'){cls='bg-red-50 text-red-600';txt='🔥 Hot'}
else if(status==='cold'){cls='bg-blue-50 text-blue-600';txt='❄️ Cold'}
else{cls='bg-amber-50 text-amber-600';txt='☀️ Warm'}
statusEl.className='text-xs px-2 py-1 rounded-full '+cls;
statusEl.textContent=txt;
document.getElementById('detailPanel').classList.remove('translate-x-full');
document.getElementById('detailPanelOverlay').classList.remove('hidden');
}
function renderPanelComments(comments) {
const el = document.getElementById('panelComments');
if (!comments || comments.length === 0) {
el.innerHTML = '<p class="text-xs text-slate-400 italic">No comments yet.</p>';
return;
}
el.innerHTML = comments.map(c => `
<div class="bg-slate-50 border border-slate-200 rounded-xl px-4 py-3">
<div class="flex items-center justify-between mb-1.5">
<span class="text-xs font-semibold text-indigo-600">Counselor</span>
<span class="text-xs text-slate-400">${escHtml(c.created_at)}</span>
</div>
<p class="text-sm text-slate-700 leading-relaxed">${escHtml(c.comment)}</p>
</div>`).join('');
}
function closeDetailPanel() {
document.getElementById('detailPanel').classList.add('translate-x-full');
document.getElementById('detailPanelOverlay').classList.add('hidden');
if (selectedRegRow) selectedRegRow.classList.remove('ring-2', 'ring-inset', 'ring-indigo-400', 'bg-indigo-50/40');
selectedRegRow = null;
selectedRegId = null;
}
function escHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
document.getElementById('panelCommentBtn').addEventListener('click', function () {
const text = document.getElementById('panelCommentText').value.trim();
if (!text || !selectedRegId) return;
this.disabled = true;
this.textContent = 'Posting…';
const btn = this;
$.ajax({
url: '{{ route("comments.store") }}',
method: 'POST',
data: { _token: '{{ csrf_token() }}', registration_id: selectedRegId, comment: text },
success: function () {
document.getElementById('panelCommentText').value = '';
$.ajax({
url: '{{ route("comments.index") }}',
data: { registration_id: selectedRegId },
success: function (r) {
const list = Array.isArray(r.comments) ? r.comments : Object.values(r.comments || {});
renderPanelComments(list.map(c => ({
comment: c.comment,
created_at: c.created_at_human ?? c.created_at,
})));
}
});
},
error: function () { alert('Failed to post comment.'); },
complete: function () {
btn.disabled = false;
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> Post Comment`;
}
});
});
document.querySelectorAll('tr[data-reg-id]').forEach(row => {
row.addEventListener('click', () => openDetailPanel(row));
});
document.getElementById('closeDetailPanel').addEventListener('click', closeDetailPanel);
document.getElementById('detailPanelOverlay').addEventListener('click', closeDetailPanel);
document.querySelector('input[name="search"]').addEventListener('input', function () {
clearTimeout(this._t);
this._t = setTimeout(() => this.closest('form').submit(), 400);
});
</script>
<script>
document.querySelectorAll('.country-select').forEach(sel => {
sel.addEventListener('change', function () {
const regId = this.dataset.regId;
const countryId = this.value;
const flagTarget = this.dataset.flagTarget;
if (!countryId) return;
// Update flag preview if present
if (flagTarget) {
const selectedOption = this.options[this.selectedIndex];
const flagUrl = selectedOption.dataset.flag;
const flagEl = document.getElementById(flagTarget);
if (flagEl && flagUrl) {
flagEl.src = flagUrl;
flagEl.classList.remove('hidden');
}
}
$.ajax({
url: '{{ route("registrations.update-country", ":id") }}'.replace(':id', regId),
method: 'POST',
data: { _token: '{{ csrf_token() }}', country_id: countryId },
error: function () { alert('Failed to update country.'); }
});
});
});
</script>
@endpush