Files
score_and_win/resources/views/registrations.blade.php
T

436 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('layouts.master')
@section('content')
<div class="max-w-7xl mx-auto px-6 py-8">
<!-- HEADER -->
<div class="flex items-center justify-between mb-7">
<div>
<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 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 class="divide-y divide-slate-100">
@foreach ($registrations as $reg)
@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();
$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.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.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>
{{-- 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.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>
<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() }}
of {{ $registrations->total() }}
</div>
<div>{{ $registrations->appends(request()->query())->links() }}</div>
</div>
</div>
</div>
<!-- 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>
<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