country + status added and other changes
This commit is contained in:
@@ -19,7 +19,8 @@ class CommentController extends Controller
|
|||||||
'comment' => $c->comment,
|
'comment' => $c->comment,
|
||||||
'author' => auth()->user()->name,
|
'author' => auth()->user()->name,
|
||||||
'created_at_human' => $c->created_at->diffForHumans(),
|
'created_at_human' => $c->created_at->diffForHumans(),
|
||||||
]);
|
])
|
||||||
|
->values(); // ← add this
|
||||||
|
|
||||||
return response()->json(['comments' => $comments]);
|
return response()->json(['comments' => $comments]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,28 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Country;
|
||||||
use App\Models\Registration;
|
use App\Models\Registration;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
|
|
||||||
return match ($user->role) {
|
return match ($user->role) {
|
||||||
'admin' => $this->adminDashboard(),
|
'admin' => $this->adminDashboard($request),
|
||||||
'counselor' => $this->counselorDashboard(),
|
'counselor' => $this->counselorDashboard($request),
|
||||||
default => abort(403, 'Unauthorized action.'),
|
default => abort(403, 'Unauthorized action.'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function adminDashboard()
|
public function adminDashboard(Request $request)
|
||||||
{
|
{
|
||||||
$today = Carbon::today();
|
$today = Carbon::today();
|
||||||
|
$search = trim($request->input('search', ''));
|
||||||
|
|
||||||
$query = Registration::with([
|
$query = Registration::with([
|
||||||
'sessions' => function ($q) use ($today) {
|
'sessions' => function ($q) use ($today) {
|
||||||
@@ -30,16 +32,21 @@ class HomeController extends Controller
|
|||||||
])
|
])
|
||||||
->whereHas('sessions', function ($q) use ($today) {
|
->whereHas('sessions', function ($q) use ($today) {
|
||||||
$q->whereDate('play_date', $today);
|
$q->whereDate('play_date', $today);
|
||||||
})
|
});
|
||||||
->orderBy('created_at', 'desc')
|
|
||||||
->paginate(10);
|
if ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('phone', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $query->orderBy('created_at', 'desc')->paginate(10)->withQueryString();
|
||||||
|
|
||||||
$registrations = $query->getCollection()->map(function ($student) {
|
$registrations = $query->getCollection()->map(function ($student) {
|
||||||
|
|
||||||
$todaySession = $student->sessions->first();
|
$todaySession = $student->sessions->first();
|
||||||
|
|
||||||
$todayGoals = null;
|
$todayGoals = null;
|
||||||
|
|
||||||
if ($todaySession) {
|
if ($todaySession) {
|
||||||
$todayGoals = $todaySession->shots
|
$todayGoals = $todaySession->shots
|
||||||
->sortBy('shot_number')
|
->sortBy('shot_number')
|
||||||
@@ -47,7 +54,6 @@ class HomeController extends Controller
|
|||||||
->values()
|
->values()
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $student->id,
|
'id' => $student->id,
|
||||||
'name' => $student->name,
|
'name' => $student->name,
|
||||||
@@ -56,41 +62,51 @@ class HomeController extends Controller
|
|||||||
'today_goals' => $todayGoals,
|
'today_goals' => $todayGoals,
|
||||||
'total_score' => $student->total_score,
|
'total_score' => $student->total_score,
|
||||||
'session_id' => $todaySession?->id,
|
'session_id' => $todaySession?->id,
|
||||||
|
'country_id' => $student->country_id,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$query->setCollection($registrations);
|
$query->setCollection($registrations);
|
||||||
|
|
||||||
$data['registrations'] = $query;
|
$data['registrations'] = $query;
|
||||||
|
$data['search'] = $search;
|
||||||
$data['total'] = Registration::count();
|
$data['total'] = Registration::count();
|
||||||
$data['played'] = $registrations->filter(fn($r) => !empty($r['today_goals']) && count($r['today_goals']) >= 3)->count();
|
$data['played'] = $registrations->filter(fn($r) => !empty($r['today_goals']) && count($r['today_goals']) >= 3)->count();
|
||||||
$data['topScore'] = $registrations->max('total_score') ?? 0;
|
$data['topScore'] = $registrations->max('total_score') ?? 0;
|
||||||
|
$data['countries'] = Country::where('status', 1)->orderBy('title')->get(['id','title','country_flag']);
|
||||||
|
|
||||||
return view('dashboard.admin', $data);
|
return view('dashboard.admin', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function counselorDashboard()
|
public function counselorDashboard(Request $request)
|
||||||
{
|
{
|
||||||
$today = Carbon::today();
|
$today = Carbon::today();
|
||||||
|
$search = trim($request->input('search', ''));
|
||||||
|
|
||||||
$query = Registration::with([
|
$query = Registration::with([
|
||||||
'sessions' => function ($q) use ($today) {
|
'sessions' => function ($q) use ($today) {
|
||||||
$q->whereDate('play_date', $today)->with('shots');
|
$q->whereDate('play_date', $today)->with('shots');
|
||||||
},
|
},
|
||||||
'comments'
|
'comments',
|
||||||
|
'country'
|
||||||
])
|
])
|
||||||
->whereHas('sessions', function ($q) use ($today) {
|
->whereHas('sessions', function ($q) use ($today) {
|
||||||
$q->whereDate('play_date', $today);
|
$q->whereDate('play_date', $today);
|
||||||
})
|
});
|
||||||
->orderBy('created_at', 'desc')
|
|
||||||
->paginate(10);
|
if ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('phone', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $query->orderBy('created_at', 'desc')->paginate(10)->withQueryString();
|
||||||
|
|
||||||
$registrations = $query->getCollection()->map(function ($student) {
|
$registrations = $query->getCollection()->map(function ($student) {
|
||||||
|
|
||||||
$todaySession = $student->sessions->first();
|
$todaySession = $student->sessions->first();
|
||||||
|
|
||||||
$todayGoals = null;
|
$todayGoals = null;
|
||||||
|
|
||||||
if ($todaySession) {
|
if ($todaySession) {
|
||||||
$todayGoals = $todaySession->shots
|
$todayGoals = $todaySession->shots
|
||||||
->sortBy('shot_number')
|
->sortBy('shot_number')
|
||||||
@@ -98,7 +114,6 @@ class HomeController extends Controller
|
|||||||
->values()
|
->values()
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $student->id,
|
'id' => $student->id,
|
||||||
'name' => $student->name,
|
'name' => $student->name,
|
||||||
@@ -107,6 +122,10 @@ class HomeController extends Controller
|
|||||||
'today_goals' => $todayGoals,
|
'today_goals' => $todayGoals,
|
||||||
'total_score' => $student->total_score,
|
'total_score' => $student->total_score,
|
||||||
'session_id' => $todaySession?->id,
|
'session_id' => $todaySession?->id,
|
||||||
|
'country_id' => $student->country_id,
|
||||||
|
'country_title' => $student->country?->title ?? '', // add
|
||||||
|
'country_flag' => $student->country?->country_flag ?? '', // add
|
||||||
|
'status' => $student->status ?? 'warm', // add if not there
|
||||||
'comment_count' => $student->comments->count(),
|
'comment_count' => $student->comments->count(),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@@ -114,9 +133,11 @@ class HomeController extends Controller
|
|||||||
$query->setCollection($registrations);
|
$query->setCollection($registrations);
|
||||||
|
|
||||||
$data['registrations'] = $query;
|
$data['registrations'] = $query;
|
||||||
|
$data['search'] = $search;
|
||||||
$data['total'] = Registration::count();
|
$data['total'] = Registration::count();
|
||||||
$data['played'] = $registrations->filter(fn($r) => !empty($r['today_goals']) && count($r['today_goals']) >= 3)->count();
|
$data['played'] = $registrations->filter(fn($r) => !empty($r['today_goals']) && count($r['today_goals']) >= 3)->count();
|
||||||
$data['topScore'] = $registrations->max('total_score') ?? 0;
|
$data['topScore'] = $registrations->max('total_score') ?? 0;
|
||||||
|
$data['countries'] = Country::where('status', 1)->orderBy('title')->get(['id','title','country_flag']);
|
||||||
|
|
||||||
return view('dashboard.counselor', $data);
|
return view('dashboard.counselor', $data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Country;
|
||||||
use App\Models\GameSession;
|
use App\Models\GameSession;
|
||||||
use App\Models\GameShot;
|
use App\Models\GameShot;
|
||||||
use App\Models\Registration;
|
use App\Models\Registration;
|
||||||
@@ -10,13 +11,28 @@ use Illuminate\Support\Facades\Cache;
|
|||||||
|
|
||||||
class RegistrationController extends Controller
|
class RegistrationController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$registrations = Registration::orderBy('id', 'desc')->paginate(20);
|
$search = trim($request->input('search', ''));
|
||||||
|
|
||||||
return view('registrations', compact('registrations'));
|
$query = Registration::with(['sessions.shots', 'comments']);
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('phone', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$registrations = $query->orderBy('id', 'desc')->paginate(20)->withQueryString();
|
||||||
|
|
||||||
|
$data['registrations'] = $registrations;
|
||||||
|
$data['search'] = $search;
|
||||||
|
$data['countries'] = Country::where('status', 1)->orderBy('title')->get(['id','title','country_flag']);
|
||||||
|
|
||||||
|
return view('registrations', $data);
|
||||||
|
}
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
return view('registrations.create');
|
return view('registrations.create');
|
||||||
@@ -312,4 +328,50 @@ class RegistrationController extends Controller
|
|||||||
return view('leaderboard', $data);
|
return view('leaderboard', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function history($id)
|
||||||
|
{
|
||||||
|
$reg = Registration::with(['sessions.shots'])->findOrFail($id);
|
||||||
|
|
||||||
|
$sessions = $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(),
|
||||||
|
])->values();
|
||||||
|
|
||||||
|
return response()->json(['sessions' => $sessions]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateCountry(Request $request, $id)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'country_id' => 'required|exists:countries,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$registration = Registration::findOrFail($id);
|
||||||
|
$registration->update(['country_id' => $request->country_id]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'ok',
|
||||||
|
'country_id' => $registration->country_id,
|
||||||
|
'country_name' => $registration->country->title,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateStatus(Request $request, $id)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'status' => 'required|in:hot,warm,cold',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$registration = Registration::findOrFail($id);
|
||||||
|
$registration->update(['status' => $request->status]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'ok',
|
||||||
|
'value' => $registration->status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Models\Registration;
|
use App\Models\Registration;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class ScoreboardController extends Controller
|
class ScoreboardController extends Controller
|
||||||
{
|
{
|
||||||
@@ -15,27 +16,33 @@ class ScoreboardController extends Controller
|
|||||||
public function select(Request $request)
|
public function select(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate(['registration_id' => 'required|exists:registrations,id']);
|
$request->validate(['registration_id' => 'required|exists:registrations,id']);
|
||||||
session(['scoreboard_player_id' => $request->registration_id]);
|
Cache::put('scoreboard_selected_player', $request->registration_id, now()->endOfDay());
|
||||||
return response()->json(['status' => 'ok']);
|
return response()->json(['status' => 'ok']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function state()
|
public function state()
|
||||||
{
|
{
|
||||||
$id = session('scoreboard_player_id');
|
$id = Cache::get('scoreboard_selected_player');
|
||||||
|
|
||||||
if (!$id) {
|
if (!$id) {
|
||||||
return response()->json(['player' => null, 'leaderboard' => $this->leaderboard()]);
|
return response()->json(['player' => null, 'leaderboard' => $this->leaderboard()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$reg = Registration::with([
|
$reg = Registration::with([
|
||||||
'sessions' => fn($q) => $q->whereDate('play_date', today())->with('shots')
|
'sessions' => fn($q) => $q->orderByDesc('play_date')->with('shots')
|
||||||
])->findOrFail($id);
|
])->findOrFail($id);
|
||||||
|
|
||||||
$todaySession = $reg->sessions->first();
|
$todaySession = $reg->sessions->firstWhere('play_date', today()->toDateString());
|
||||||
$shots = $todaySession
|
$shots = $todaySession
|
||||||
? $todaySession->shots->sortBy('shot_number')->map(fn($s) => (bool)$s->result)->values()->toArray()
|
? $todaySession->shots->sortBy('shot_number')->map(fn($s) => (bool)$s->result)->values()->toArray()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
$history = $reg->sessions->map(fn($s) => [
|
||||||
|
'date' => $s->play_date,
|
||||||
|
'score' => $s->score,
|
||||||
|
'shots' => $s->shots->sortBy('shot_number')->map(fn($sh) => (bool)$sh->result)->values()->toArray(),
|
||||||
|
])->toArray();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'player' => [
|
'player' => [
|
||||||
'id' => $reg->id,
|
'id' => $reg->id,
|
||||||
@@ -43,6 +50,7 @@ class ScoreboardController extends Controller
|
|||||||
'total_score' => $reg->total_score,
|
'total_score' => $reg->total_score,
|
||||||
'shots' => $shots,
|
'shots' => $shots,
|
||||||
'session_score' => $todaySession?->calculateScore() ?? 0,
|
'session_score' => $todaySession?->calculateScore() ?? 0,
|
||||||
|
'history' => $history,
|
||||||
],
|
],
|
||||||
'leaderboard' => $this->leaderboard(),
|
'leaderboard' => $this->leaderboard(),
|
||||||
]);
|
]);
|
||||||
@@ -55,6 +63,7 @@ class ScoreboardController extends Controller
|
|||||||
->get(['id', 'name', 'total_score'])
|
->get(['id', 'name', 'total_score'])
|
||||||
->map(fn($r, $i) => [
|
->map(fn($r, $i) => [
|
||||||
'rank' => $i + 1,
|
'rank' => $i + 1,
|
||||||
|
'id' => $r->id,
|
||||||
'name' => $r->name,
|
'name' => $r->name,
|
||||||
'total_score' => $r->total_score,
|
'total_score' => $r->total_score,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Country extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['title', 'country_flag', 'status'];
|
||||||
|
|
||||||
|
public function registrations()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Registration::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,6 +10,8 @@ class Registration extends Model
|
|||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
|
'country_id',
|
||||||
|
'status',
|
||||||
'total_score',
|
'total_score',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -31,6 +33,11 @@ class Registration extends Model
|
|||||||
return $this->hasMany(Comment::class);
|
return $this->hasMany(Comment::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function country()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\Country::class);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|---------------------------
|
|---------------------------
|
||||||
| Helper methods
|
| Helper methods
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('countries', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('country_flag')->nullable();
|
||||||
|
$table->boolean('status')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('countries');
|
||||||
|
}
|
||||||
|
};
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('registrations', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('country_id')->nullable();
|
||||||
|
$table->enum('status', ['hot', 'warm', 'cold'])->default('warm');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('registrations', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['country_id', 'status']);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -15,11 +15,14 @@ class DatabaseSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
|
$this->call([
|
||||||
|
countries_seeder::class,
|
||||||
|
]);
|
||||||
// User::factory(10)->create();
|
// User::factory(10)->create();
|
||||||
|
|
||||||
User::factory()->create([
|
// User::factory()->create([
|
||||||
'name' => 'Test User',
|
// 'name' => 'Test User',
|
||||||
'email' => 'test@example.com',
|
// 'email' => 'test@example.com',
|
||||||
]);
|
// ]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class countries_seeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
DB::table('countries')->truncate();
|
||||||
|
|
||||||
|
DB::table('countries')->insert([
|
||||||
|
[
|
||||||
|
'id' => 1,
|
||||||
|
'title' => 'United States',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/us.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 2,
|
||||||
|
'title' => 'Mexico',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/mx.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 3,
|
||||||
|
'title' => 'Canada',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/ca.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 4,
|
||||||
|
'title' => 'Japan',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/jp.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 5,
|
||||||
|
'title' => 'Iran',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/ir.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 6,
|
||||||
|
'title' => 'South Korea',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/kr.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 7,
|
||||||
|
'title' => 'Australia',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/au.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 8,
|
||||||
|
'title' => 'Saudi Arabia',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/sa.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 9,
|
||||||
|
'title' => 'Qatar',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/qa.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 10,
|
||||||
|
'title' => 'Uzbekistan',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/uz.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 11,
|
||||||
|
'title' => 'Jordan',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/jo.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 12,
|
||||||
|
'title' => 'Iraq',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/iq.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 13,
|
||||||
|
'title' => 'Argentina',
|
||||||
|
'country_flag' => 'countries/ZQLg2ALI9Ez25ZqiFA2u5LRox2lmn66MOkr9Ywqr.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 14,
|
||||||
|
'title' => 'Brazil',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/br.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 15,
|
||||||
|
'title' => 'Uruguay',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/uy.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 16,
|
||||||
|
'title' => 'Colombia',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/co.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 17,
|
||||||
|
'title' => 'Ecuador',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/ec.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 18,
|
||||||
|
'title' => 'Paraguay',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/py.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 19,
|
||||||
|
'title' => 'New Zealand',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/nz.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 20,
|
||||||
|
'title' => 'Morocco',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/ma.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 21,
|
||||||
|
'title' => 'Senegal',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/sn.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 22,
|
||||||
|
'title' => 'Egypt',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/eg.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 23,
|
||||||
|
'title' => 'Algeria',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/dz.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 24,
|
||||||
|
'title' => 'Tunisia',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/tn.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 25,
|
||||||
|
'title' => 'South Africa',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/za.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 26,
|
||||||
|
'title' => 'Ivory Coast',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/ci.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 27,
|
||||||
|
'title' => 'Ghana',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/gh.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 28,
|
||||||
|
'title' => 'Cape Verde',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/cv.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 29,
|
||||||
|
'title' => 'DR Congo',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/cd.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 30,
|
||||||
|
'title' => 'England',
|
||||||
|
'country_flag' => 'https://upload.wikimedia.org/wikipedia/en/thumb/b/be/Flag_of_England.svg/120px-Flag_of_England.svg.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 31,
|
||||||
|
'title' => 'France',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/fr.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 32,
|
||||||
|
'title' => 'Spain',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/es.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 33,
|
||||||
|
'title' => 'Germany',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/de.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 34,
|
||||||
|
'title' => 'Portugal',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/pt.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 35,
|
||||||
|
'title' => 'Netherlands',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/nl.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 36,
|
||||||
|
'title' => 'Belgium',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/be.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 37,
|
||||||
|
'title' => 'Croatia',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/hr.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 38,
|
||||||
|
'title' => 'Switzerland',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/ch.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 39,
|
||||||
|
'title' => 'Austria',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/at.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 40,
|
||||||
|
'title' => 'Scotland',
|
||||||
|
'country_flag' => 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Flag_of_Scotland.svg/120px-Flag_of_Scotland.svg.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 41,
|
||||||
|
'title' => 'Norway',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/no.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 42,
|
||||||
|
'title' => 'Bosnia and Herzegovina',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/ba.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 43,
|
||||||
|
'title' => 'Sweden',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/se.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 44,
|
||||||
|
'title' => 'Türkiye',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/tr.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 45,
|
||||||
|
'title' => 'Czechia',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/cz.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 46,
|
||||||
|
'title' => 'Panama',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/pa.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 47,
|
||||||
|
'title' => 'Curaçao',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/cw.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 48,
|
||||||
|
'title' => 'Haiti',
|
||||||
|
'country_flag' => 'https://flagcdn.com/w80/ht.png',
|
||||||
|
'status' => 1,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,19 +7,29 @@
|
|||||||
<div class="flex items-center justify-between mb-7">
|
<div class="flex items-center justify-between mb-7">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-slate-900 text-xl font-bold tracking-tight">Student Registrations</h1>
|
<h1 class="text-slate-900 text-xl font-bold tracking-tight">Student Registrations</h1>
|
||||||
<p class="text-slate-500 text-sm mt-0.5">Track registrations and goal scores for today's session.</p>
|
<p class="text-slate-500 text-sm mt-0.5">View registrations and manage student comments.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
|
<!-- Search -->
|
||||||
|
<form method="GET" action="{{ route('home') }}" 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('home') }}" 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>
|
||||||
<a href="{{ route('registrations.index') }}"
|
<a href="{{ route('registrations.index') }}"
|
||||||
class="flex items-center gap-2 bg-white hover:bg-slate-50 border border-slate-200 text-slate-700 text-sm font-medium px-4 py-2 rounded-lg transition-colors shadow-sm">
|
class="flex items-center gap-2 bg-white hover:bg-slate-50 border border-slate-200 text-slate-700 text-sm font-medium px-4 py-2 rounded-lg transition-colors shadow-sm">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
<path d="M3 6h18"/><path d="M3 12h18"/><path d="M3 18h18"/>
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M3 6h18"></path>
|
|
||||||
<path d="M3 12h18"></path>
|
|
||||||
<path d="M3 18h18"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
All Registrations
|
All Registrations
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('leaderboard') }}"
|
<a href="{{ route('leaderboard') }}"
|
||||||
@@ -29,13 +39,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Leaderboard
|
Leaderboard
|
||||||
</a>
|
</a>
|
||||||
<button id="newRegistrationBtn"
|
|
||||||
class="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors shadow-sm">
|
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
||||||
</svg>
|
|
||||||
New Registration
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -96,6 +99,7 @@
|
|||||||
<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">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">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">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">Today's Shots</th>
|
<th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Today's Shots</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">Total Score</th>
|
||||||
<th class="px-5 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
|
<th class="px-5 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
|
||||||
@@ -123,6 +127,19 @@
|
|||||||
</td>
|
</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['phone'] }}</td>
|
||||||
<td class="px-5 py-3.5 text-slate-500">{{ $reg['email'] }}</td>
|
<td class="px-5 py-3.5 text-slate-500">{{ $reg['email'] }}</td>
|
||||||
|
<td class="px-5 py-3.5" onclick="event.stopPropagation()">
|
||||||
|
<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'] }}">
|
||||||
|
<option value="">— Select —</option>
|
||||||
|
@foreach($countries as $country)
|
||||||
|
<option value="{{ $country->id }}"
|
||||||
|
{{ ($reg['country_id'] ?? null) == $country->id ? 'selected' : '' }}>
|
||||||
|
{{ $country->title }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
<td class="px-5 py-3.5">
|
<td class="px-5 py-3.5">
|
||||||
<div class="shots-cell flex items-center justify-center gap-1.5">
|
<div class="shots-cell flex items-center justify-center gap-1.5">
|
||||||
@if(is_null($reg['today_goals']))
|
@if(is_null($reg['today_goals']))
|
||||||
@@ -775,4 +792,33 @@ document.getElementById('submitRegistrationBtn').addEventListener('click', funct
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</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
|
@endpush
|
||||||
@@ -10,6 +10,20 @@
|
|||||||
<p class="text-slate-500 text-sm mt-0.5">View registrations and manage student comments.</p>
|
<p class="text-slate-500 text-sm mt-0.5">View registrations and manage student comments.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
|
<form method="GET" action="{{ route('home') }}" 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('home') }}" 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>
|
||||||
<a href="{{ route('registrations.index') }}"
|
<a href="{{ route('registrations.index') }}"
|
||||||
class="flex items-center gap-2 bg-white hover:bg-slate-50 border border-slate-200 text-slate-700 text-sm font-medium px-4 py-2 rounded-lg transition-colors shadow-sm">
|
class="flex items-center gap-2 bg-white hover:bg-slate-50 border border-slate-200 text-slate-700 text-sm font-medium px-4 py-2 rounded-lg transition-colors shadow-sm">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -84,6 +98,8 @@
|
|||||||
<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">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">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">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">Today's Shots</th>
|
<th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Today's Shots</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">Total Score</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">Comments</th>
|
||||||
@@ -91,9 +107,16 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-slate-100">
|
<tbody class="divide-y divide-slate-100">
|
||||||
@foreach ($registrations as $reg)
|
@foreach ($registrations as $reg)
|
||||||
<tr class="hover:bg-slate-50/70 transition-colors group cursor-pointer"
|
<tr class="hover:bg-slate-50 cursor-pointer"
|
||||||
data-reg-id="{{ $reg['id'] }}"
|
data-reg-id="{{ $reg['id'] }}"
|
||||||
data-name="{{ $reg['name'] }}">
|
data-name="{{ $reg['name'] }}"
|
||||||
|
data-phone="{{ $reg['phone'] }}"
|
||||||
|
data-email="{{ $reg['email'] ?? '' }}"
|
||||||
|
data-score="{{ $reg['total_score'] }}"
|
||||||
|
data-status="{{ $reg['status'] ?? 'warm' }}"
|
||||||
|
data-country-title="{{ $reg['country_title'] ?? '' }}"
|
||||||
|
data-country-flag="{{ $reg['country_flag'] ?? '' }}"
|
||||||
|
data-shots="{{ json_encode($reg['today_goals'] ?? []) }}">
|
||||||
<td class="px-5 py-3.5">
|
<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>
|
<span class="text-xs font-mono text-slate-400">#{{ str_pad($reg['id'], 4, '0', STR_PAD_LEFT) }}</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -107,6 +130,28 @@
|
|||||||
</td>
|
</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['phone'] }}</td>
|
||||||
<td class="px-5 py-3.5 text-slate-500">{{ $reg['email'] }}</td>
|
<td class="px-5 py-3.5 text-slate-500">{{ $reg['email'] }}</td>
|
||||||
|
|
||||||
|
<!-- COUNTRY -->
|
||||||
|
<td class="px-5 py-3" onclick="event.stopPropagation()">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
@if(!empty($reg['country_flag']))
|
||||||
|
<img src="{{ $reg['country_flag'] }}" class="w-5 h-3.5 rounded-sm">
|
||||||
|
@endif
|
||||||
|
<span class="text-slate-600 text-sm">{{ $reg['country_title'] ?: '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- STATUS -->
|
||||||
|
<td class="px-5 py-3 text-center" onclick="event.stopPropagation()">
|
||||||
|
@php $status = $reg['status'] ?? 'warm'; @endphp
|
||||||
|
<select class="status-select text-xs px-2 py-1 rounded border cursor-pointer"
|
||||||
|
data-reg-id="{{ $reg['id'] }}">
|
||||||
|
<option value="hot" {{ $status === 'hot' ? 'selected' : '' }}>🔥 Hot</option>
|
||||||
|
<option value="warm" {{ $status === 'warm' ? 'selected' : '' }}>☀️ Warm</option>
|
||||||
|
<option value="cold" {{ $status === 'cold' ? 'selected' : '' }}>❄️ Cold</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="px-5 py-3.5">
|
<td class="px-5 py-3.5">
|
||||||
<div class="flex items-center justify-center gap-1.5">
|
<div class="flex items-center justify-center gap-1.5">
|
||||||
@if(is_null($reg['today_goals']))
|
@if(is_null($reg['today_goals']))
|
||||||
@@ -126,12 +171,14 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="px-5 py-3.5 text-center">
|
<td class="px-5 py-3.5 text-center">
|
||||||
<div class="inline-flex items-center gap-1 font-bold text-slate-900">
|
<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>
|
<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>
|
<span class="text-base">{{ $reg['total_score'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="px-5 py-3.5 text-center">
|
<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">
|
<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">
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -151,20 +198,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- COMMENT SIDE PANEL -->
|
<!-- STUDENT DETAIL SIDE PANEL -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<div id="commentPanel"
|
<div id="detailPanel"
|
||||||
class="fixed top-0 right-0 h-full w-96 bg-white border-l border-slate-200 shadow-2xl z-40
|
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
|
transform translate-x-full transition-transform duration-300 ease-in-out
|
||||||
flex flex-col">
|
flex flex-col">
|
||||||
|
|
||||||
<!-- Panel Header -->
|
<!-- Panel Header -->
|
||||||
<div class="px-5 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50 shrink-0">
|
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50 flex items-center justify-between shrink-0">
|
||||||
<div>
|
<div class="flex items-center gap-3">
|
||||||
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Comments for</p>
|
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center shrink-0">
|
||||||
<p id="commentPanelName" class="text-slate-900 font-bold text-base mt-0.5">—</p>
|
<span id="panelInitial" class="text-sm font-bold text-indigo-600">—</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="closeCommentPanel"
|
<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 hidden rounded-sm">
|
||||||
|
<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">
|
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">
|
<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"/>
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
@@ -172,36 +229,58 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Comment History -->
|
<!-- Student Info Strip -->
|
||||||
<div id="commentList" class="flex-1 overflow-y-auto px-5 py-4 space-y-3 min-h-0">
|
<div class="px-6 py-4 border-b border-slate-100 shrink-0">
|
||||||
<!-- filled by JS -->
|
<div class="grid grid-cols-3 gap-3">
|
||||||
<div id="commentLoading" class="flex items-center justify-center h-full text-slate-400 text-sm">
|
<div class="bg-slate-50 rounded-xl px-3 py-3 text-center border border-slate-100">
|
||||||
<svg class="animate-spin mr-2" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0"/></svg>
|
<p class="text-xs text-slate-400 font-medium mb-1">Total Score</p>
|
||||||
Loading comments…
|
<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 id="commentEmpty" class="hidden flex-col items-center justify-center h-full text-center py-12">
|
|
||||||
<div class="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mx-auto mb-3">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.75" 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>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-slate-500 text-sm font-medium">No comments yet</p>
|
<div class="bg-slate-50 rounded-xl px-3 py-3 text-center border border-slate-100">
|
||||||
<p class="text-slate-400 text-xs mt-1">Add the first comment below.</p>
|
<p class="text-xs text-slate-400 font-medium mb-1">Today's Score</p>
|
||||||
|
<p id="panelTodayScore" class="text-xl font-black text-slate-900">—</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Comment Input -->
|
<!-- Scrollable content -->
|
||||||
<div class="px-5 py-4 border-t border-slate-100 bg-slate-50 shrink-0">
|
<div class="flex-1 overflow-y-auto min-h-0">
|
||||||
<label class="block text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Add Comment</label>
|
|
||||||
<textarea id="newCommentText" rows="3"
|
<!-- Today's Shots -->
|
||||||
placeholder="Write a comment about this student…"
|
<div class="px-6 py-4 border-b border-slate-100">
|
||||||
class="w-full border border-slate-200 rounded-xl px-3 py-2.5 text-sm text-slate-800 bg-white
|
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Today's Shots</p>
|
||||||
|
<div id="panelTodayShots" class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-slate-400 italic">Not played today.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Score History -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Comments -->
|
||||||
|
<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"></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
|
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500
|
||||||
transition-all placeholder-slate-400 resize-none"></textarea>
|
transition-all placeholder-slate-400 resize-none"></textarea>
|
||||||
<button id="submitCommentBtn"
|
<button id="panelCommentBtn"
|
||||||
class="mt-2.5 w-full bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800
|
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">
|
||||||
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">
|
<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"/>
|
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -209,9 +288,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Overlay -->
|
<!-- Overlay -->
|
||||||
<div id="commentPanelOverlay" class="fixed inset-0 bg-black/20 z-30 hidden"></div>
|
<div id="detailPanelOverlay" class="fixed inset-0 bg-black/20 z-30 hidden"></div>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@@ -220,117 +301,175 @@
|
|||||||
let selectedRegId = null;
|
let selectedRegId = null;
|
||||||
let selectedRegRow = null;
|
let selectedRegRow = null;
|
||||||
|
|
||||||
// ── Open panel ───────────────────────────────────────────────
|
function openDetailPanel(row) {
|
||||||
function openCommentPanel(row) {
|
|
||||||
if (selectedRegRow) selectedRegRow.classList.remove('ring-2', 'ring-inset', 'ring-indigo-400', 'bg-indigo-50/40');
|
if (selectedRegRow) selectedRegRow.classList.remove('ring-2', 'ring-inset', 'ring-indigo-400', 'bg-indigo-50/40');
|
||||||
selectedRegRow = row;
|
selectedRegRow = row;
|
||||||
row.classList.add('ring-2', 'ring-inset', 'ring-indigo-400', 'bg-indigo-50/40');
|
row.classList.add('ring-2', 'ring-inset', 'ring-indigo-400', 'bg-indigo-50/40');
|
||||||
|
|
||||||
selectedRegId = row.dataset.regId;
|
selectedRegId = row.dataset.regId;
|
||||||
document.getElementById('commentPanelName').textContent = row.dataset.name;
|
const name = row.dataset.name;
|
||||||
document.getElementById('newCommentText').value = '';
|
const phone = row.dataset.phone;
|
||||||
|
const email = row.dataset.email;
|
||||||
|
const score = row.dataset.score;
|
||||||
|
const todayShots = JSON.parse(row.dataset.shots || '[]');
|
||||||
|
const countryTitle = row.dataset.countryTitle || '';
|
||||||
|
const countryFlag = row.dataset.countryFlag || '';
|
||||||
|
const status = row.dataset.status || 'warm';
|
||||||
|
|
||||||
document.getElementById('commentPanel').classList.remove('translate-x-full');
|
// Header
|
||||||
document.getElementById('commentPanelOverlay').classList.remove('hidden');
|
document.getElementById('panelInitial').textContent = name.charAt(0).toUpperCase();
|
||||||
|
document.getElementById('panelName').textContent = name;
|
||||||
|
document.getElementById('panelPhone').textContent = phone;
|
||||||
|
|
||||||
loadComments();
|
// Info strip
|
||||||
|
document.getElementById('panelScore').textContent = score;
|
||||||
|
document.getElementById('panelEmail').textContent = email || '—';
|
||||||
|
|
||||||
|
// Country
|
||||||
|
const flag = document.getElementById('panelCountryFlag');
|
||||||
|
if (countryFlag && countryFlag !== 'null' && countryFlag !== '') {
|
||||||
|
flag.src = countryFlag;
|
||||||
|
flag.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
flag.classList.add('hidden');
|
||||||
|
}
|
||||||
|
document.getElementById('panelCountryTitle').textContent = countryTitle || '—';
|
||||||
|
|
||||||
|
// Status badge
|
||||||
|
const statusEl = document.getElementById('panelStatus');
|
||||||
|
if (status === 'hot') {
|
||||||
|
statusEl.className = 'text-xs px-2 py-1 rounded bg-red-50 text-red-600';
|
||||||
|
statusEl.textContent = '🔥 Hot';
|
||||||
|
} else if (status === 'cold') {
|
||||||
|
statusEl.className = 'text-xs px-2 py-1 rounded bg-blue-50 text-blue-600';
|
||||||
|
statusEl.textContent = '❄️ Cold';
|
||||||
|
} else {
|
||||||
|
statusEl.className = 'text-xs px-2 py-1 rounded bg-amber-50 text-amber-600';
|
||||||
|
statusEl.textContent = '☀️ Warm';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeCommentPanel() {
|
// Today's shots
|
||||||
document.getElementById('commentPanel').classList.add('translate-x-full');
|
const shotsEl = document.getElementById('panelTodayShots');
|
||||||
document.getElementById('commentPanelOverlay').classList.add('hidden');
|
if (!todayShots || todayShots.length === 0) {
|
||||||
|
shotsEl.innerHTML = '<span class="text-xs text-slate-400 italic">Not played today.</span>';
|
||||||
|
document.getElementById('panelTodayScore').textContent = '—';
|
||||||
|
} else {
|
||||||
|
const todayGoals = todayShots.filter(Boolean).length;
|
||||||
|
document.getElementById('panelTodayScore').textContent = todayGoals;
|
||||||
|
shotsEl.innerHTML = todayShots.map((g, i) => g
|
||||||
|
? `<div class="w-8 h-8 rounded-full bg-emerald-100 border-2 border-emerald-400 flex items-center justify-center" title="Shot ${i+1}: Goal">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
|
</div>`
|
||||||
|
: `<div class="w-8 h-8 rounded-full bg-slate-100 border-2 border-slate-300 flex items-center justify-center" title="Shot ${i+1}: Miss">
|
||||||
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" 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>
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
document.getElementById('panelHistory').innerHTML = '<p class="text-xs text-slate-400 italic">Loading…</p>';
|
||||||
|
document.getElementById('panelComments').innerHTML = '<p class="text-xs text-slate-400 italic">Loading…</p>';
|
||||||
|
document.getElementById('panelCommentText').value = '';
|
||||||
|
|
||||||
|
// Load comments
|
||||||
|
$.ajax({
|
||||||
|
url: '{{ route("comments.index") }}',
|
||||||
|
data: { registration_id: selectedRegId },
|
||||||
|
success: function (r) {
|
||||||
|
renderPanelComments((r.comments || []).map(c => ({
|
||||||
|
comment: c.comment,
|
||||||
|
created_at: c.created_at_human ?? c.created_at,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load session history
|
||||||
|
$.get('/registrations/' + selectedRegId + '/history', function (r) {
|
||||||
|
renderHistory(r.sessions || []);
|
||||||
|
}).fail(function () {
|
||||||
|
document.getElementById('panelHistory').innerHTML = '<p class="text-xs text-slate-400 italic">No sessions found.</p>';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('detailPanel').classList.remove('translate-x-full');
|
||||||
|
document.getElementById('detailPanelOverlay').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHistory(sessions) {
|
||||||
|
const el = document.getElementById('panelHistory');
|
||||||
|
if (!sessions || sessions.length === 0) {
|
||||||
|
el.innerHTML = '<p class="text-xs text-slate-400 italic">No sessions yet.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = sessions.map(s => {
|
||||||
|
const shots = [0, 1, 2].map(i => {
|
||||||
|
if (s.shots[i] === undefined) return `<span class="text-slate-300 text-sm">⚽</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">${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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
if (selectedRegRow) selectedRegRow.classList.remove('ring-2', 'ring-inset', 'ring-indigo-400', 'bg-indigo-50/40');
|
||||||
selectedRegRow = null;
|
selectedRegRow = null;
|
||||||
selectedRegId = null;
|
selectedRegId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load comments ─────────────────────────────────────────────
|
|
||||||
function loadComments() {
|
|
||||||
const list = document.getElementById('commentList');
|
|
||||||
const loading = document.getElementById('commentLoading');
|
|
||||||
const empty = document.getElementById('commentEmpty');
|
|
||||||
|
|
||||||
// Clear previous comments (keep loading/empty nodes)
|
|
||||||
list.querySelectorAll('.comment-item').forEach(el => el.remove());
|
|
||||||
loading.classList.remove('hidden');
|
|
||||||
empty.classList.add('hidden');
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: '{{ route("comments.index") }}',
|
|
||||||
method: 'GET',
|
|
||||||
data: { registration_id: selectedRegId },
|
|
||||||
success: function (res) {
|
|
||||||
loading.classList.add('hidden');
|
|
||||||
|
|
||||||
if (!res.comments || res.comments.length === 0) {
|
|
||||||
empty.classList.remove('hidden');
|
|
||||||
empty.classList.add('flex');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
empty.classList.add('hidden');
|
|
||||||
res.comments.forEach(c => appendComment(c, false));
|
|
||||||
list.scrollTop = list.scrollHeight;
|
|
||||||
},
|
|
||||||
error: function () {
|
|
||||||
loading.classList.add('hidden');
|
|
||||||
loading.textContent = 'Failed to load comments.';
|
|
||||||
loading.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Render a single comment bubble ───────────────────────────
|
|
||||||
function appendComment(c, scrollInto = true) {
|
|
||||||
const list = document.getElementById('commentList');
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.className = 'comment-item bg-slate-50 border border-slate-200 rounded-xl px-4 py-3';
|
|
||||||
el.innerHTML = `
|
|
||||||
<div class="flex items-center justify-between mb-1.5">
|
|
||||||
<span class="text-xs font-semibold text-indigo-600">${escHtml(c.author ?? 'Counselor')}</span>
|
|
||||||
<span class="text-xs text-slate-400">${escHtml(c.created_at_human ?? c.created_at ?? '')}</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-slate-700 leading-relaxed">${escHtml(c.comment)}</p>`;
|
|
||||||
list.appendChild(el);
|
|
||||||
if (scrollInto) list.scrollTop = list.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escHtml(str) {
|
function escHtml(str) {
|
||||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Submit comment ────────────────────────────────────────────
|
document.getElementById('panelCommentBtn').addEventListener('click', function () {
|
||||||
document.getElementById('submitCommentBtn').addEventListener('click', function () {
|
const text = document.getElementById('panelCommentText').value.trim();
|
||||||
const text = document.getElementById('newCommentText').value.trim();
|
|
||||||
if (!text || !selectedRegId) return;
|
if (!text || !selectedRegId) return;
|
||||||
|
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
this.textContent = 'Posting…';
|
this.textContent = 'Posting…';
|
||||||
|
|
||||||
const btn = this;
|
const btn = this;
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '{{ route("comments.store") }}',
|
url: '{{ route("comments.store") }}',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: { _token: '{{ csrf_token() }}', registration_id: selectedRegId, comment: text },
|
||||||
_token: '{{ csrf_token() }}',
|
success: function () {
|
||||||
registration_id: selectedRegId,
|
document.getElementById('panelCommentText').value = '';
|
||||||
comment: text,
|
$.ajax({
|
||||||
},
|
url: '{{ route("comments.index") }}',
|
||||||
success: function (res) {
|
data: { registration_id: selectedRegId },
|
||||||
document.getElementById('newCommentText').value = '';
|
success: function (r) {
|
||||||
document.getElementById('commentEmpty').classList.add('hidden');
|
renderPanelComments((r.comments || []).map(c => ({
|
||||||
|
comment: c.comment, created_at: c.created_at_human ?? c.created_at,
|
||||||
if (selectedRegRow) {
|
})));
|
||||||
const badge = selectedRegRow.querySelector('.comment-count');
|
|
||||||
if (badge) badge.textContent = parseInt(badge.textContent || '0') + 1;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
appendComment(res.comment);
|
|
||||||
},
|
|
||||||
error: function () {
|
|
||||||
alert('Failed to post comment. Please try again.');
|
|
||||||
},
|
},
|
||||||
|
error: function () { alert('Failed to post comment.'); },
|
||||||
complete: function () {
|
complete: function () {
|
||||||
btn.disabled = false;
|
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`;
|
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`;
|
||||||
@@ -338,12 +477,44 @@ document.getElementById('submitCommentBtn').addEventListener('click', function (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Row clicks ───────────────────────────────────────────────
|
|
||||||
document.querySelectorAll('tr[data-reg-id]').forEach(row => {
|
document.querySelectorAll('tr[data-reg-id]').forEach(row => {
|
||||||
row.addEventListener('click', () => openCommentPanel(row));
|
row.addEventListener('click', () => openDetailPanel(row));
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('closeCommentPanel').addEventListener('click', closeCommentPanel);
|
document.getElementById('closeDetailPanel').addEventListener('click', closeDetailPanel);
|
||||||
document.getElementById('commentPanelOverlay').addEventListener('click', closeCommentPanel);
|
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>
|
||||||
|
const statusColors = {
|
||||||
|
hot: 'bg-red-50 text-red-600 border-red-200',
|
||||||
|
warm: 'bg-amber-50 text-amber-600 border-amber-200',
|
||||||
|
cold: 'bg-blue-50 text-blue-600 border-blue-200',
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('.status-select').forEach(sel => {
|
||||||
|
sel.addEventListener('change', function () {
|
||||||
|
const regId = this.dataset.regId;
|
||||||
|
const status = this.value;
|
||||||
|
const el = this;
|
||||||
|
|
||||||
|
Object.values(statusColors).forEach(c =>
|
||||||
|
c.split(' ').forEach(cls => el.classList.remove(cls))
|
||||||
|
);
|
||||||
|
statusColors[status].split(' ').forEach(cls => el.classList.add(cls));
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '{{ route("registrations.update-status", ":id") }}'.replace(':id', regId),
|
||||||
|
method: 'POST',
|
||||||
|
data: { _token: '{{ csrf_token() }}', status: status },
|
||||||
|
error: function () { alert('Failed to update status.'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
@@ -51,6 +51,13 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelector('input[name="search"]').addEventListener('input', function () {
|
||||||
|
clearTimeout(this._t);
|
||||||
|
this._t = setTimeout(() => this.closest('form').submit(), 400);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
@stack('js')
|
@stack('js')
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -9,71 +9,428 @@
|
|||||||
<h1 class="text-slate-900 text-xl font-bold">All Registrations</h1>
|
<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>
|
<p class="text-slate-500 text-sm mt-0.5">Complete list of registered users</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- TABLE -->
|
<!-- TABLE -->
|
||||||
<div class="bg-white border rounded-xl overflow-hidden">
|
<div class="bg-white border rounded-xl overflow-hidden">
|
||||||
|
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
|
|
||||||
<thead class="bg-slate-50 border-b">
|
<thead class="bg-slate-50 border-b">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-5 py-3 text-left">ID</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">Name</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">Phone</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">Email</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-center">Total Score</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">Created</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
<tbody>
|
|
||||||
@foreach ($registrations as $reg)
|
@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>
|
$commentData = $reg->comments->sortByDesc('created_at')->map(fn($c) => [
|
||||||
|
'comment' => $c->comment,
|
||||||
<td class="px-5 py-3 font-medium">
|
'created_at' => $c->created_at->diffForHumans(),
|
||||||
{{ $reg->name ?? '-' }}
|
])->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>
|
</td>
|
||||||
|
|
||||||
<td class="px-5 py-3">
|
{{-- Status TD (read-only badge) --}}
|
||||||
{{ $reg->phone }}
|
<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>
|
||||||
|
|
||||||
<td class="px-5 py-3">
|
<td class="px-5 py-3.5 text-center">
|
||||||
{{ $reg->email ?? '-' }}
|
<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>
|
||||||
|
<td class="px-5 py-3.5 text-center">
|
||||||
<td class="px-5 py-3 text-center font-bold">
|
<span class="inline-flex items-center gap-1 text-xs font-medium text-slate-500 bg-slate-100 px-2 py-1 rounded-full">
|
||||||
{{ $reg->total_score }}
|
<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>
|
||||||
|
<td class="px-5 py-3.5 text-center">
|
||||||
<td class="px-5 py-3 text-center text-slate-500">
|
<span class="inline-flex items-center gap-1 text-xs font-medium text-slate-500 bg-slate-100 px-2 py-1 rounded-full">
|
||||||
{{ $reg->created_at->format('Y-m-d') }}
|
<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>
|
||||||
|
<td class="px-5 py-3.5 text-center text-slate-500 text-xs">{{ $reg->created_at->format('Y-m-d') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- PAGINATION -->
|
|
||||||
<div class="px-5 py-3 border-t flex justify-between items-center">
|
<div class="px-5 py-3 border-t flex justify-between items-center">
|
||||||
|
|
||||||
<div class="text-xs text-slate-500">
|
<div class="text-xs text-slate-500">
|
||||||
Showing {{ $registrations->firstItem() }} - {{ $registrations->lastItem() }}
|
Showing {{ $registrations->firstItem() }}–{{ $registrations->lastItem() }}
|
||||||
of {{ $registrations->total() }}
|
of {{ $registrations->total() }}
|
||||||
</div>
|
</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>
|
<div>
|
||||||
{{ $registrations->links() }}
|
<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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div id="detailPanelOverlay" class="fixed inset-0 bg-black/20 z-30 hidden"></div>
|
||||||
|
|
||||||
@endsection
|
@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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -78,13 +78,27 @@
|
|||||||
.bar i { display:block; height:100%; background:linear-gradient(90deg,#0566e9,#0ab4ff,#0640c3); transition:width .5s ease; }
|
.bar i { display:block; height:100%; background:linear-gradient(90deg,#0566e9,#0ab4ff,#0640c3); transition:width .5s ease; }
|
||||||
.rscore { color:var(--cyan); font-size:clamp(14px,1.9vw,26px); font-weight:900; text-align:right; }
|
.rscore { color:var(--cyan); font-size:clamp(14px,1.9vw,26px); font-weight:900; text-align:right; }
|
||||||
.pts { font-size:clamp(8px,.9vw,13px); font-family:Arial,sans-serif; }
|
.pts { font-size:clamp(8px,.9vw,13px); font-family:Arial,sans-serif; }
|
||||||
.next-box { text-align:center; cursor:default; justify-content:center; align-items:center; gap:1vh; padding:1.5vh 0; border:none; background:rgba(1,17,35,.85); border:1.5px solid var(--cyan); }
|
|
||||||
.next-ball { width:9.5vw; height:9.5vw; border-radius:50%; display:grid; place-items:center; font-size:clamp(32px,5.5vw,80px); background:radial-gradient(circle,rgba(0,174,239,.3),transparent 62%); box-shadow:0 0 24px rgba(0,174,239,.55); }
|
/* HISTORY BOX */
|
||||||
.waiting-text { font-family:Arial,sans-serif; font-size:clamp(11px,1.4vw,20px); font-weight:900; text-transform:uppercase; letter-spacing:.5px; color:var(--cyan); }
|
.next-box { padding:.8vh 1.2vw; align-items:stretch; justify-content:flex-start; gap:0; }
|
||||||
|
.history-wrap { flex:1; min-height:0; overflow-y:auto; display:flex; flex-direction:column; gap:.5vh; padding-top:.3vh; justify-content:flex-start; }
|
||||||
|
.history-wrap::-webkit-scrollbar { width:3px; }
|
||||||
|
.history-wrap::-webkit-scrollbar-thumb { background:rgba(0,174,239,.4); border-radius:99px; }
|
||||||
|
.history-row { display:grid; grid-template-columns:6vw 1fr auto; align-items:center; gap:.6vw; padding:.55vh .5vw; border-radius:7px; background:rgba(0,174,239,.06); border:1px solid rgba(0,174,239,.15); flex-shrink:0; }
|
||||||
|
.history-date { font-family:Arial,sans-serif; font-size:clamp(9px,1vw,13px); font-weight:900; color:var(--cyan); line-height:1.25; }
|
||||||
|
.history-shots { display:flex; gap:.35vw; align-items:center; }
|
||||||
|
.hshot { font-size:clamp(11px,1.2vw,17px); line-height:1; }
|
||||||
|
.hshot.goal { filter:drop-shadow(0 0 4px rgba(0,255,70,.6)); }
|
||||||
|
.hshot.miss { color:var(--red); }
|
||||||
|
.hshot.pending { opacity:.25; }
|
||||||
|
.history-score { font-family:Arial,sans-serif; font-size:clamp(13px,1.5vw,22px); font-weight:900; color:#fff; white-space:nowrap; }
|
||||||
|
.history-score span { color:var(--cyan); font-size:clamp(8px,.9vw,12px); }
|
||||||
|
.history-today { border-color:rgba(0,174,239,.5); background:rgba(0,174,239,.12); }
|
||||||
|
.no-history { font-family:Arial,sans-serif; font-size:clamp(10px,1.1vw,15px); color:rgba(255,255,255,.3); text-align:center; margin:auto; text-transform:uppercase; letter-spacing:.5px; }
|
||||||
|
|
||||||
.footer { flex-shrink:0; width:50vw; margin:0 auto; padding:.65vh 2vw; display:flex; justify-content:center; gap:3vw; background:rgba(1,17,35,.82); border:1px solid rgba(0,174,239,.5); border-radius:12px 12px 0 0; font-family:Arial,sans-serif; font-size:clamp(9px,1vw,14px); font-weight:900; text-transform:uppercase; }
|
.footer { flex-shrink:0; width:50vw; margin:0 auto; padding:.65vh 2vw; display:flex; justify-content:center; gap:3vw; background:rgba(1,17,35,.82); border:1px solid rgba(0,174,239,.5); border-radius:12px 12px 0 0; font-family:Arial,sans-serif; font-size:clamp(9px,1vw,14px); font-weight:900; text-transform:uppercase; }
|
||||||
.footer span:first-child { color:var(--cyan); }
|
.footer span:first-child { color:var(--cyan); }
|
||||||
|
|
||||||
/* pulse on score update */
|
|
||||||
@keyframes scorePop { 0%{transform:scale(1)} 50%{transform:scale(1.2)} 100%{transform:scale(1)} }
|
@keyframes scorePop { 0%{transform:scale(1)} 50%{transform:scale(1.2)} 100%{transform:scale(1)} }
|
||||||
.score-pop { animation:scorePop .35s ease; }
|
.score-pop { animation:scorePop .35s ease; }
|
||||||
</style>
|
</style>
|
||||||
@@ -138,15 +152,15 @@
|
|||||||
<section class="bottom-row">
|
<section class="bottom-row">
|
||||||
<div class="leaderboard">
|
<div class="leaderboard">
|
||||||
<div class="section-title"><span>Top 5 Leaderboard</span></div>
|
<div class="section-title"><span>Top 5 Leaderboard</span></div>
|
||||||
<div class="ranks-wrap" id="leaderboardRows">
|
<div class="ranks-wrap" id="leaderboardRows"></div>
|
||||||
<!-- filled by JS -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- CHANGED: was next-box with ball, now session history -->
|
||||||
<div class="next-box">
|
<div class="next-box">
|
||||||
<div class="section-title"><span>Now Playing</span></div>
|
<div class="section-title"><span>Player History</span></div>
|
||||||
<div class="next-ball">⚽</div>
|
<div class="history-wrap" id="historyWrap">
|
||||||
<div class="waiting-text" id="waitingText">Select a player</div>
|
<div class="no-history">Select a player</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -161,59 +175,49 @@
|
|||||||
<script>
|
<script>
|
||||||
const POLL_URL = "{{ route('scoreboard.state') }}";
|
const POLL_URL = "{{ route('scoreboard.state') }}";
|
||||||
const MEDALS = ['🏆','🥈','🥉'];
|
const MEDALS = ['🏆','🥈','🥉'];
|
||||||
|
const TODAY = new Date().toISOString().slice(0, 10);
|
||||||
let lastPlayerId = null;
|
let lastPlayerId = null;
|
||||||
let lastShots = [];
|
let lastShots = [];
|
||||||
let lastTotal = null;
|
let lastTotal = null;
|
||||||
let lastLeaderStr = '';
|
let lastLeaderStr = '';
|
||||||
|
let lastHistoryStr = '';
|
||||||
|
|
||||||
function renderShot(index, result) {
|
function renderShot(index, result) {
|
||||||
// index 0-based
|
|
||||||
const n = index + 1;
|
const n = index + 1;
|
||||||
const icon = document.getElementById('shotIcon' + n);
|
const icon = document.getElementById('shotIcon' + n);
|
||||||
const res = document.getElementById('shotResult' + n);
|
const res = document.getElementById('shotResult' + n);
|
||||||
const pts = document.getElementById('shotPts' + n);
|
const pts = document.getElementById('shotPts' + n);
|
||||||
|
|
||||||
if (result === true) {
|
if (result === true) {
|
||||||
icon.className = 'shot-icon goal';
|
icon.className = 'shot-icon goal'; icon.textContent = '⚽';
|
||||||
icon.textContent = '⚽';
|
res.className = 'result goal-badge'; res.textContent = 'Goal';
|
||||||
res.className = 'result goal-badge';
|
|
||||||
res.textContent = 'Goal';
|
|
||||||
pts.textContent = '+1 PT';
|
pts.textContent = '+1 PT';
|
||||||
} else if (result === false) {
|
} else if (result === false) {
|
||||||
icon.className = 'shot-icon miss';
|
icon.className = 'shot-icon miss'; icon.textContent = '✕';
|
||||||
icon.textContent = '✕';
|
res.className = 'result miss-badge'; res.textContent = 'Miss';
|
||||||
res.className = 'result miss-badge';
|
|
||||||
res.textContent = 'Miss';
|
|
||||||
pts.textContent = '0 PTS';
|
pts.textContent = '0 PTS';
|
||||||
} else {
|
} else {
|
||||||
icon.className = 'shot-icon pending';
|
icon.className = 'shot-icon pending'; icon.textContent = '⚽';
|
||||||
icon.textContent = '⚽';
|
res.className = 'result pending-badge'; res.textContent = '—';
|
||||||
res.className = 'result pending-badge';
|
|
||||||
res.textContent = '—';
|
|
||||||
pts.textContent = '0 PTS';
|
pts.textContent = '0 PTS';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetShots() {
|
function resetShots() { for (let i = 0; i < 3; i++) renderShot(i, null); }
|
||||||
for (let i = 0; i < 3; i++) renderShot(i, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function popScore(el) {
|
function popScore(el) {
|
||||||
el.classList.remove('score-pop');
|
el.classList.remove('score-pop');
|
||||||
void el.offsetWidth; // reflow
|
void el.offsetWidth;
|
||||||
el.classList.add('score-pop');
|
el.classList.add('score-pop');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLeaderboard(rows, activeId) {
|
function renderLeaderboard(rows, activeId) {
|
||||||
const wrap = document.getElementById('leaderboardRows');
|
const wrap = document.getElementById('leaderboardRows');
|
||||||
const maxScore = rows[0]?.total_score || 1;
|
const maxScore = rows[0]?.total_score || 1;
|
||||||
|
|
||||||
wrap.innerHTML = rows.map((r, i) => {
|
wrap.innerHTML = rows.map((r, i) => {
|
||||||
const medal = i < 3 ? `<div class="medal">${MEDALS[i]}</div>` : `<div class="rank-num">${r.rank}</div>`;
|
const medal = i < 3 ? `<div class="medal">${MEDALS[i]}</div>` : `<div class="rank-num">${r.rank}</div>`;
|
||||||
const pct = Math.round((r.total_score / maxScore) * 100);
|
const pct = Math.round((r.total_score / maxScore) * 100);
|
||||||
const active = r.id === activeId ? 'active-player' : '';
|
const active = r.id === activeId ? 'active-player' : '';
|
||||||
return `
|
return `<div class="rank ${active}">
|
||||||
<div class="rank ${active}">
|
|
||||||
${medal}
|
${medal}
|
||||||
<div class="rname">${r.name}</div>
|
<div class="rname">${r.name}</div>
|
||||||
<div class="bar"><i style="width:${pct}%"></i></div>
|
<div class="bar"><i style="width:${pct}%"></i></div>
|
||||||
@@ -223,46 +227,66 @@
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderHistory(history) {
|
||||||
|
const wrap = document.getElementById('historyWrap');
|
||||||
|
const str = JSON.stringify(history);
|
||||||
|
if (str === lastHistoryStr) return;
|
||||||
|
lastHistoryStr = str;
|
||||||
|
|
||||||
|
if (!history || history.length === 0) {
|
||||||
|
wrap.innerHTML = '<div class="no-history">No sessions yet</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap.innerHTML = history.map(s => {
|
||||||
|
const isToday = s.date === TODAY;
|
||||||
|
const shots = [0, 1, 2].map(i => {
|
||||||
|
if (s.shots[i] === undefined) return `<span class="hshot pending">⚽</span>`;
|
||||||
|
return s.shots[i]
|
||||||
|
? `<span class="hshot goal">⚽</span>`
|
||||||
|
: `<span class="hshot miss">✕</span>`;
|
||||||
|
}).join('');
|
||||||
|
return `<div class="history-row ${isToday ? 'history-today' : ''}">
|
||||||
|
<div class="history-date">${s.date}</div>
|
||||||
|
<div class="history-shots">${shots}</div>
|
||||||
|
<div class="history-score">${s.score} <span>PTS</span></div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
function poll() {
|
function poll() {
|
||||||
fetch(POLL_URL)
|
fetch(POLL_URL)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
||||||
// ── Leaderboard ──────────────────────────────
|
|
||||||
const lbStr = JSON.stringify(data.leaderboard);
|
const lbStr = JSON.stringify(data.leaderboard);
|
||||||
if (lbStr !== lastLeaderStr) {
|
if (lbStr !== lastLeaderStr) {
|
||||||
renderLeaderboard(data.leaderboard, data.player?.id);
|
renderLeaderboard(data.leaderboard, data.player?.id);
|
||||||
lastLeaderStr = lbStr;
|
lastLeaderStr = lbStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Player ───────────────────────────────────
|
|
||||||
if (!data.player) {
|
if (!data.player) {
|
||||||
if (lastPlayerId !== null) {
|
if (lastPlayerId !== null) {
|
||||||
document.getElementById('playerName').textContent = 'Waiting for player…';
|
document.getElementById('playerName').textContent = 'Waiting for player…';
|
||||||
document.getElementById('totalScore').textContent = '—';
|
document.getElementById('totalScore').textContent = '—';
|
||||||
document.getElementById('waitingText').textContent = 'Select a player';
|
document.getElementById('historyWrap').innerHTML = '<div class="no-history">Select a player</div>';
|
||||||
|
lastHistoryStr = '';
|
||||||
resetShots();
|
resetShots();
|
||||||
lastPlayerId = null;
|
lastPlayerId = null; lastShots = []; lastTotal = null;
|
||||||
lastShots = [];
|
|
||||||
lastTotal = null;
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = data.player;
|
const p = data.player;
|
||||||
|
|
||||||
// New player selected
|
|
||||||
if (p.id !== lastPlayerId) {
|
if (p.id !== lastPlayerId) {
|
||||||
document.getElementById('playerName').textContent = p.name.toUpperCase();
|
document.getElementById('playerName').textContent = p.name.toUpperCase();
|
||||||
document.getElementById('waitingText').textContent = p.name;
|
|
||||||
resetShots();
|
resetShots();
|
||||||
lastPlayerId = p.id;
|
lastPlayerId = p.id; lastShots = []; lastTotal = null;
|
||||||
lastShots = [];
|
lastHistoryStr = '';
|
||||||
lastTotal = null;
|
|
||||||
renderLeaderboard(data.leaderboard, p.id);
|
renderLeaderboard(data.leaderboard, p.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total score
|
|
||||||
if (p.total_score !== lastTotal) {
|
if (p.total_score !== lastTotal) {
|
||||||
const el = document.getElementById('totalScore');
|
const el = document.getElementById('totalScore');
|
||||||
el.textContent = p.total_score;
|
el.textContent = p.total_score;
|
||||||
@@ -270,20 +294,18 @@
|
|||||||
lastTotal = p.total_score;
|
lastTotal = p.total_score;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update shots one by one
|
|
||||||
p.shots.forEach((result, i) => {
|
p.shots.forEach((result, i) => {
|
||||||
if (lastShots[i] === undefined) {
|
if (lastShots[i] === undefined) renderShot(i, result);
|
||||||
renderShot(i, result);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
lastShots = [...p.shots];
|
lastShots = [...p.shots];
|
||||||
|
|
||||||
|
renderHistory(p.history);
|
||||||
})
|
})
|
||||||
.catch(() => {}); // silently ignore network errors
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll every 3 seconds
|
|
||||||
poll();
|
poll();
|
||||||
setInterval(poll, 3000);
|
setInterval(poll, 2000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+3
-1
@@ -32,7 +32,9 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::get('/{id}/json', [RegistrationController::class, 'getRegistrationJson'])->name('get-json');
|
Route::get('/{id}/json', [RegistrationController::class, 'getRegistrationJson'])->name('get-json');
|
||||||
Route::post('/record-shot', [RegistrationController::class, 'recordShot'])->name('record-shot');
|
Route::post('/record-shot', [RegistrationController::class, 'recordShot'])->name('record-shot');
|
||||||
Route::post('/record-shots', [RegistrationController::class, 'recordShots'])->name('record-shots');
|
Route::post('/record-shots', [RegistrationController::class, 'recordShots'])->name('record-shots');
|
||||||
|
Route::get('/{id}/history', [RegistrationController::class, 'history'])->name('history');
|
||||||
|
Route::post('/{id}/country', [RegistrationController::class, 'updateCountry'])->name('update-country');
|
||||||
|
Route::post('/{id}/status', [RegistrationController::class, 'updateStatus'])->name('update-status');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::resource('comments', CommentController::class)->only(['index', 'store'])->names('comments');
|
Route::resource('comments', CommentController::class)->only(['index', 'store'])->names('comments');
|
||||||
|
|||||||
Reference in New Issue
Block a user