added affiliation tab card with its information

This commit is contained in:
Ghassan Yusuf 2026-01-24 14:52:55 +03:00
parent 352d54a788
commit c1fc28087e
14 changed files with 1493 additions and 47 deletions

28
TODO_affiliations.md Normal file
View File

@ -0,0 +1,28 @@
# TODO: Implement Affiliations Tab
## Backend Implementation
- [x] Create ClubAffiliation model and migration
- [x] Create SkillAcquisition model and migration
- [x] Create AffiliationMedia model and migration
- [x] Update User model with clubAffiliations relationship
- [x] Update FamilyController::profile() to fetch affiliations data and summary stats
- [x] Add club_affiliation_id to TournamentEvent model and migration
## Frontend Implementation
- [x] Update show.blade.php affiliations tab content
- [x] Implement horizontal timeline with clickable nodes
- [x] Add dynamic skills wheel using Chart.js Polar Area
- [x] Create affiliation details panel
- [x] Add summary stats above timeline
- [x] Ensure responsive design (desktop side-by-side, mobile stacked)
- [x] Add Alpine.js for timeline interactions
- [x] Implement keyboard navigation and accessibility
- [x] Add club affiliation column to tournament table
## Testing & Deployment
- [x] Run migrations to create database tables
- [x] Add sample data for demonstration
- [x] Add calculated duration display to timeline cards (as badges)
- [x] Test timeline navigation and skills wheel updates
- [x] Verify responsive design on different screen sizes
- [x] Add "Add Tournament Participation" button with modal functionality

View File

@ -9,6 +9,7 @@ use App\Models\Invoice;
use App\Models\TournamentEvent;
use App\Models\Goal;
use App\Models\Attendance;
use App\Models\ClubAffiliation;
use App\Services\FamilyService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -60,7 +61,7 @@ class FamilyController extends Controller
// Fetch tournament data
$tournamentEvents = $user->tournamentEvents()
->with(['performanceResults', 'notesMedia'])
->with(['performanceResults', 'notesMedia', 'clubAffiliation'])
->orderBy('date', 'desc')
->get();
@ -88,6 +89,24 @@ class FamilyController extends Controller
$totalSessions = $attendanceRecords->count();
$attendanceRate = $totalSessions > 0 ? round(($sessionsCompleted / $totalSessions) * 100, 1) : 0;
// Fetch affiliations data
$clubAffiliations = $user->clubAffiliations()
->with(['skillAcquisitions', 'affiliationMedia'])
->orderBy('start_date', 'desc')
->get();
// Add icon_class to media items for JavaScript
$clubAffiliations->each(function($affiliation) {
$affiliation->affiliationMedia->each(function($media) {
$media->icon_class = $media->icon_class;
});
});
// Calculate summary stats
$totalAffiliations = $clubAffiliations->count();
$distinctSkills = $clubAffiliations->flatMap->skillAcquisitions->pluck('skill_name')->unique()->count();
$totalMembershipDuration = $clubAffiliations->sum('duration_in_months');
// Pass user directly and a flag to indicate it's the current user's profile
return view('family.show', [
'relationship' => (object)[
@ -111,6 +130,10 @@ class FamilyController extends Controller
'sessionsCompleted' => $sessionsCompleted,
'noShows' => $noShows,
'attendanceRate' => $attendanceRate,
'clubAffiliations' => $clubAffiliations,
'totalAffiliations' => $totalAffiliations,
'distinctSkills' => $distinctSkills,
'totalMembershipDuration' => $totalMembershipDuration,
]);
}
@ -280,7 +303,7 @@ class FamilyController extends Controller
// Fetch tournament data for the dependent
$tournamentEvents = $relationship->dependent->tournamentEvents()
->with(['performanceResults', 'notesMedia'])
->with(['performanceResults', 'notesMedia', 'clubAffiliation'])
->orderBy('date', 'desc')
->get();
@ -308,7 +331,46 @@ class FamilyController extends Controller
$totalSessions = $attendanceRecords->count();
$attendanceRate = $totalSessions > 0 ? round(($sessionsCompleted / $totalSessions) * 100, 1) : 0;
return view('family.show', compact('relationship', 'latestHealthRecord', 'healthRecords', 'comparisonRecords', 'invoices', 'tournamentEvents', 'awardCounts', 'sports', 'goals', 'activeGoalsCount', 'completedGoalsCount', 'successRate', 'attendanceRecords', 'sessionsCompleted', 'noShows', 'attendanceRate'));
// Fetch affiliations data for the dependent
$clubAffiliations = $relationship->dependent->clubAffiliations()
->with(['skillAcquisitions', 'affiliationMedia'])
->orderBy('start_date', 'desc')
->get();
// Add icon_class to media items for JavaScript
$clubAffiliations->each(function($affiliation) {
$affiliation->affiliationMedia->each(function($media) {
$media->icon_class = $media->icon_class;
});
});
// Calculate summary stats
$totalAffiliations = $clubAffiliations->count();
$distinctSkills = $clubAffiliations->flatMap->skillAcquisitions->pluck('skill_name')->unique()->count();
$totalMembershipDuration = $clubAffiliations->sum('duration_in_months');
return view('family.show', [
'relationship' => $relationship,
'latestHealthRecord' => $latestHealthRecord,
'healthRecords' => $healthRecords,
'comparisonRecords' => $comparisonRecords,
'invoices' => $invoices,
'tournamentEvents' => $tournamentEvents,
'awardCounts' => $awardCounts,
'sports' => $sports,
'goals' => $goals,
'activeGoalsCount' => $activeGoalsCount,
'completedGoalsCount' => $completedGoalsCount,
'successRate' => $successRate,
'attendanceRecords' => $attendanceRecords,
'sessionsCompleted' => $sessionsCompleted,
'noShows' => $noShows,
'attendanceRate' => $attendanceRate,
'clubAffiliations' => $clubAffiliations,
'totalAffiliations' => $totalAffiliations,
'distinctSkills' => $distinctSkills,
'totalMembershipDuration' => $totalMembershipDuration,
]);
}
/**
@ -634,6 +696,81 @@ class FamilyController extends Controller
return response()->json(['success' => true, 'message' => 'Goal updated successfully']);
}
/**
* Store a new tournament participation record.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function storeTournament(Request $request, $id)
{
$user = Auth::user();
// Check if user is authorized to add tournament for this dependent
if ($user->id !== (int)$id) {
$relationship = UserRelationship::where('guardian_user_id', $user->id)
->where('dependent_user_id', $id)
->first();
if (!$relationship) {
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
}
}
// Validate the request
$validated = $request->validate([
'title' => 'required|string|max:255',
'type' => 'required|in:championship,tournament,competition,exhibition',
'sport' => 'required|string|max:100',
'date' => 'required|date',
'time' => 'nullable|date_format:H:i',
'location' => 'nullable|string|max:255',
'participants_count' => 'nullable|integer|min:1',
'club_affiliation_id' => 'nullable|exists:club_affiliations,id',
'performance_results' => 'nullable|array',
'performance_results.*.medal_type' => 'nullable|in:special,1st,2nd,3rd',
'performance_results.*.points' => 'nullable|numeric|min:0',
'performance_results.*.description' => 'nullable|string|max:500',
'notes_media' => 'nullable|array',
'notes_media.*.note_text' => 'nullable|string|max:1000',
'notes_media.*.media_link' => 'nullable|url',
]);
// Create the tournament event
$tournament = TournamentEvent::create([
'user_id' => $id,
'club_affiliation_id' => $validated['club_affiliation_id'] ?? null,
'title' => $validated['title'],
'type' => $validated['type'],
'sport' => $validated['sport'],
'date' => $validated['date'],
'time' => $validated['time'],
'location' => $validated['location'],
'participants_count' => $validated['participants_count'],
]);
// Create performance results
if (isset($validated['performance_results'])) {
foreach ($validated['performance_results'] as $resultData) {
if (!empty($resultData['medal_type'])) {
$tournament->performanceResults()->create($resultData);
}
}
}
// Create notes and media
if (isset($validated['notes_media'])) {
foreach ($validated['notes_media'] as $noteData) {
if (!empty($noteData['note_text']) || !empty($noteData['media_link'])) {
$tournament->notesMedia()->create($noteData);
}
}
}
return response()->json(['success' => true, 'message' => 'Tournament record added successfully']);
}
/**
* Remove the specified family member from storage.
*

View File

@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AffiliationMedia extends Model
{
protected $fillable = [
'club_affiliation_id',
'media_type',
'media_url',
'title',
'description',
];
/**
* Get the club affiliation that owns the media.
*/
public function clubAffiliation(): BelongsTo
{
return $this->belongsTo(ClubAffiliation::class);
}
/**
* Get the full URL for the media.
*/
public function getFullUrlAttribute(): string
{
if (filter_var($this->media_url, FILTER_VALIDATE_URL)) {
return $this->media_url;
}
return asset('storage/' . $this->media_url);
}
/**
* Get icon class for media type.
*/
public function getIconClassAttribute(): string
{
return match($this->media_type) {
'certificate' => 'bi-file-earmark-text',
'photo' => 'bi-image',
'video' => 'bi-play-circle',
'document' => 'bi-file-text',
default => 'bi-file',
};
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ClubAffiliation extends Model
{
protected $fillable = [
'member_id',
'club_name',
'logo',
'start_date',
'end_date',
'location',
'coaches',
'description',
];
protected $casts = [
'start_date' => 'date',
'end_date' => 'date',
'coaches' => 'array',
];
/**
* Get the member that owns the affiliation.
*/
public function member(): BelongsTo
{
return $this->belongsTo(User::class, 'member_id');
}
/**
* Get the skills acquired during this affiliation.
*/
public function skillAcquisitions(): HasMany
{
return $this->hasMany(SkillAcquisition::class);
}
/**
* Get the media associated with this affiliation.
*/
public function affiliationMedia(): HasMany
{
return $this->hasMany(AffiliationMedia::class);
}
/**
* Get the duration of the affiliation in months.
*/
public function getDurationInMonthsAttribute(): int
{
$endDate = $this->end_date ?? now();
return $this->start_date->diffInMonths($endDate);
}
/**
* Get formatted date range.
*/
public function getDateRangeAttribute(): string
{
$start = $this->start_date->format('M Y');
$end = $this->end_date ? $this->end_date->format('M Y') : 'Present';
return $start . ' ' . $end;
}
/**
* Get detailed formatted duration (years, months, days).
*/
public function getFormattedDurationAttribute(): string
{
$endDate = $this->end_date ?? now();
$diff = $this->start_date->diff($endDate);
$parts = [];
if ($diff->y > 0) {
$parts[] = $diff->y . ' year' . ($diff->y > 1 ? 's' : '');
}
if ($diff->m > 0) {
$parts[] = $diff->m . ' month' . ($diff->m > 1 ? 's' : '');
}
if ($diff->d > 0) {
$parts[] = $diff->d . ' day' . ($diff->d > 1 ? 's' : '');
}
return implode(' ', $parts) ?: 'Same day';
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SkillAcquisition extends Model
{
protected $fillable = [
'club_affiliation_id',
'skill_name',
'icon',
'duration_months',
'proficiency_level',
];
protected $casts = [
'duration_months' => 'integer',
];
/**
* Get the club affiliation that owns the skill acquisition.
*/
public function clubAffiliation(): BelongsTo
{
return $this->belongsTo(ClubAffiliation::class);
}
/**
* Get formatted duration.
*/
public function getFormattedDurationAttribute(): string
{
$months = $this->duration_months;
if ($months < 12) {
return $months . ' month' . ($months > 1 ? 's' : '');
}
$years = floor($months / 12);
$remainingMonths = $months % 12;
$result = $years . ' year' . ($years > 1 ? 's' : '');
if ($remainingMonths > 0) {
$result .= ' ' . $remainingMonths . ' month' . ($remainingMonths > 1 ? 's' : '');
}
return $result;
}
/**
* Get proficiency level color for UI.
*/
public function getProficiencyColorAttribute(): string
{
return match($this->proficiency_level) {
'beginner' => 'text-blue-500',
'intermediate' => 'text-yellow-500',
'advanced' => 'text-orange-500',
'expert' => 'text-red-500',
default => 'text-gray-500',
};
}
}

View File

@ -10,6 +10,7 @@ class TournamentEvent extends Model
{
protected $fillable = [
'user_id',
'club_affiliation_id',
'title',
'type',
'sport',
@ -29,6 +30,11 @@ class TournamentEvent extends Model
return $this->belongsTo(User::class);
}
public function clubAffiliation(): BelongsTo
{
return $this->belongsTo(ClubAffiliation::class);
}
public function performanceResults(): HasMany
{
return $this->hasMany(PerformanceResult::class);

View File

@ -250,6 +250,14 @@ class User extends Authenticatable
return $this->hasMany(Attendance::class, 'member_id');
}
/**
* Get the club affiliations for the user.
*/
public function clubAffiliations(): HasMany
{
return $this->hasMany(ClubAffiliation::class, 'member_id');
}
/**
* Send the email verification notification.
* Override to prevent sending the default Laravel notification.

View File

@ -0,0 +1,37 @@
<?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('club_affiliations', function (Blueprint $table) {
$table->id();
$table->foreignId('member_id')->constrained('users')->onDelete('cascade');
$table->string('club_name');
$table->string('logo')->nullable();
$table->date('start_date');
$table->date('end_date')->nullable();
$table->string('location')->nullable();
$table->json('coaches')->nullable(); // Array of coach names
$table->text('description')->nullable();
$table->timestamps();
$table->index(['member_id', 'start_date']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('club_affiliations');
}
};

View File

@ -0,0 +1,34 @@
<?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('skill_acquisitions', function (Blueprint $table) {
$table->id();
$table->foreignId('club_affiliation_id')->constrained('club_affiliations')->onDelete('cascade');
$table->string('skill_name');
$table->string('icon')->nullable(); // FontAwesome or Heroicons class
$table->integer('duration_months');
$table->enum('proficiency_level', ['beginner', 'intermediate', 'advanced', 'expert'])->default('beginner');
$table->timestamps();
$table->index(['club_affiliation_id', 'skill_name']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('skill_acquisitions');
}
};

View File

@ -0,0 +1,34 @@
<?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('affiliation_media', function (Blueprint $table) {
$table->id();
$table->foreignId('club_affiliation_id')->constrained('club_affiliations')->onDelete('cascade');
$table->enum('media_type', ['certificate', 'photo', 'video', 'document']);
$table->string('media_url');
$table->string('title')->nullable();
$table->text('description')->nullable();
$table->timestamps();
$table->index(['club_affiliation_id', 'media_type']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('affiliation_media');
}
};

View File

@ -0,0 +1,28 @@
<?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('tournament_events', function (Blueprint $table) {
$table->foreignId('club_affiliation_id')->nullable()->constrained('club_affiliations')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('tournament_events', function (Blueprint $table) {
//
});
}
};

View File

@ -0,0 +1,160 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Tenant;
use App\Models\ClubAffiliation;
use App\Models\SkillAcquisition;
use App\Models\AffiliationMedia;
use App\Models\TournamentEvent;
use Carbon\Carbon;
class AffiliationSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Get the first user or create a sample one
$user = User::first();
if (!$user) {
return; // No users to seed affiliations for
}
// Create additional clubs if they don't exist
$clubs = [
[
'club_name' => 'Elite Boxing Club',
'slug' => 'elite-boxing-club',
'gps_lat' => 25.276987,
'gps_long' => 55.296249,
],
[
'club_name' => 'Zen Martial Arts Academy',
'slug' => 'zen-martial-arts-academy',
'gps_lat' => 25.286987,
'gps_long' => 55.306249,
],
[
'club_name' => 'Power Fitness Gym',
'slug' => 'power-fitness-gym',
'gps_lat' => 25.266987,
'gps_long' => 55.286249,
],
];
$createdClubs = [];
foreach ($clubs as $clubData) {
$club = Tenant::firstOrCreate(
['slug' => $clubData['slug']],
array_merge($clubData, ['owner_user_id' => $user->id])
);
$createdClubs[] = $club;
}
// Sample club affiliations
$affiliations = [
[
'club_name' => 'Elite Boxing Club',
'start_date' => Carbon::parse('2020-01-15'),
'end_date' => Carbon::parse('2021-12-31'),
'location' => 'Downtown Fitness Center',
'coaches' => ['Coach Mike Johnson', 'Coach Sarah Davis'],
'description' => 'Premier boxing training facility focusing on technique and conditioning.',
'logo' => 'https://via.placeholder.com/100x100/FF6B6B/FFFFFF?text=EBC',
'skills' => [
['skill_name' => 'Boxing', 'icon' => 'fas fa-fist-raised', 'duration_months' => 18, 'proficiency_level' => 'advanced'],
['skill_name' => 'Fitness Training', 'icon' => 'fas fa-dumbbell', 'duration_months' => 12, 'proficiency_level' => 'intermediate'],
['skill_name' => 'Footwork', 'icon' => 'fas fa-shoe-prints', 'duration_months' => 15, 'proficiency_level' => 'advanced'],
],
'media' => [
['media_type' => 'certificate', 'title' => 'Boxing Certification', 'media_url' => 'https://via.placeholder.com/300x200/4ECDC4/FFFFFF?text=Boxing+Cert', 'description' => 'Advanced Boxing Certificate'],
['media_type' => 'photo', 'title' => 'Championship Photo', 'media_url' => 'https://via.placeholder.com/300x200/45B7D1/FFFFFF?text=Championship', 'description' => 'Regional Championship 2021'],
]
],
[
'club_name' => 'Zen Martial Arts Academy',
'start_date' => Carbon::parse('2018-03-01'),
'end_date' => Carbon::parse('2020-01-10'),
'location' => 'East Side Dojo',
'coaches' => ['Master Chen Wei', 'Instructor Lisa Park'],
'description' => 'Traditional martial arts academy specializing in multiple disciplines.',
'logo' => 'https://via.placeholder.com/100x100/96CEB4/FFFFFF?text=ZMA',
'skills' => [
['skill_name' => 'Taekwondo', 'icon' => 'fas fa-hand-rock', 'duration_months' => 20, 'proficiency_level' => 'expert'],
['skill_name' => 'Karate', 'icon' => 'fas fa-fist-raised', 'duration_months' => 16, 'proficiency_level' => 'advanced'],
['skill_name' => 'Self Defense', 'icon' => 'fas fa-shield-alt', 'duration_months' => 18, 'proficiency_level' => 'advanced'],
],
'media' => [
['media_type' => 'certificate', 'title' => 'Black Belt Certificate', 'media_url' => 'https://via.placeholder.com/300x200/FECA57/FFFFFF?text=Black+Belt', 'description' => 'Taekwondo Black Belt Certification'],
]
],
[
'club_name' => 'Power Fitness Gym',
'start_date' => Carbon::parse('2022-06-01'),
'end_date' => null, // Current affiliation
'location' => 'West End Sports Complex',
'coaches' => ['Trainer Alex Rodriguez', 'Trainer Emma Wilson'],
'description' => 'Modern fitness center with comprehensive training programs.',
'logo' => 'https://via.placeholder.com/100x100/FFEAA7/000000?text=PFG',
'skills' => [
['skill_name' => 'Weight Training', 'icon' => 'fas fa-dumbbell', 'duration_months' => 8, 'proficiency_level' => 'intermediate'],
['skill_name' => 'Cardio Fitness', 'icon' => 'fas fa-heartbeat', 'duration_months' => 6, 'proficiency_level' => 'beginner'],
['skill_name' => 'Nutrition', 'icon' => 'fas fa-apple-alt', 'duration_months' => 4, 'proficiency_level' => 'beginner'],
],
'media' => [
['media_type' => 'photo', 'title' => 'Gym Progress Photo', 'media_url' => 'https://via.placeholder.com/300x200/DD5E89/FFFFFF?text=Progress', 'description' => 'Before and after transformation'],
]
],
];
$createdAffiliations = [];
foreach ($affiliations as $affiliationData) {
$skills = $affiliationData['skills'];
$media = $affiliationData['media'];
unset($affiliationData['skills'], $affiliationData['media']);
$affiliationData['member_id'] = $user->id;
$affiliation = ClubAffiliation::create($affiliationData);
$createdAffiliations[] = $affiliation;
// Create skills
foreach ($skills as $skillData) {
$affiliation->skillAcquisitions()->create($skillData);
}
// Create media
foreach ($media as $mediaData) {
$affiliation->affiliationMedia()->create($mediaData);
}
}
// Link some tournament events to affiliations
$tournamentEvents = TournamentEvent::where('user_id', $user->id)->get();
if ($tournamentEvents->count() > 0 && count($createdAffiliations) > 0) {
// Link first tournament to Elite Boxing Club affiliation
$boxingAffiliation = collect($createdAffiliations)->firstWhere('club_name', 'Elite Boxing Club');
if ($boxingAffiliation) {
$boxingEvents = $tournamentEvents->where('sport', 'Boxing');
foreach ($boxingEvents as $event) {
$event->update(['club_affiliation_id' => $boxingAffiliation->id]);
}
}
// Link martial arts events to Zen Martial Arts Academy
$martialArtsAffiliation = collect($createdAffiliations)->firstWhere('club_name', 'Zen Martial Arts Academy');
if ($martialArtsAffiliation) {
$martialArtsEvents = $tournamentEvents->whereIn('sport', ['Taekwondo', 'Karate', 'Martial Arts']);
foreach ($martialArtsEvents as $event) {
$event->update(['club_affiliation_id' => $martialArtsAffiliation->id]);
}
}
}
}
}

View File

@ -66,7 +66,7 @@
<li><a class="dropdown-item" href="#"><i class="bi bi-calendar-check me-2"></i>Add Attendance Record</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-calendar-event me-2"></i>Add Event Participation</a></li>
<li><a class="dropdown-item" href="#" data-bs-target="#healthUpdateModal"><i class="bi bi-heart-pulse me-2"></i>Add Health Update</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-award me-2"></i>Add Tournament Participation</a></li>
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#tournamentParticipationModal"><i class="bi bi-award me-2"></i>Add Tournament Participation</a></li>
<li><a class="dropdown-item" href="@if($relationship->relationship_type == 'self'){{ route('profile.edit') }}@else{{ route('family.edit', $relationship->dependent->id) }}@endif">
<i class="bi bi-pencil me-2"></i>Edit Info
</a></li>
@ -86,12 +86,12 @@
<!-- Achievement Badges -->
<div class="d-flex gap-2 mb-3 flex-wrap">
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none" style="font-size: 1rem;">🏆 <span class="fw-semibold text-dark">3</span></a>
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none" style="font-size: 1rem;">🥇 <span class="fw-semibold text-dark">4</span></a>
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none" style="font-size: 1rem;">🥈 <span class="fw-semibold text-dark">6</span></a>
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none" style="font-size: 1rem;">🥉 <span class="fw-semibold text-dark">3</span></a>
<a href="#goals" class="border bg-white rounded px-2 py-1 text-decoration-none" style="font-size: 1rem;" onclick="document.getElementById('goals-tab').click();">🎯 <span class="fw-semibold text-dark">8</span></a>
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none" style="font-size: 1rem;"> <span class="fw-semibold text-dark">12</span></a>
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none achievement-badge" style="font-size: 1rem;" data-medal-type="special" onclick="filterTournamentsByMedal('special')">🏆 <span class="fw-semibold text-dark">{{ $awardCounts['special'] }}</span></a>
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none achievement-badge" style="font-size: 1rem;" data-medal-type="1st" onclick="filterTournamentsByMedal('1st')">🥇 <span class="fw-semibold text-dark">{{ $awardCounts['1st'] }}</span></a>
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none achievement-badge" style="font-size: 1rem;" data-medal-type="2nd" onclick="filterTournamentsByMedal('2nd')">🥈 <span class="fw-semibold text-dark">{{ $awardCounts['2nd'] }}</span></a>
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none achievement-badge" style="font-size: 1rem;" data-medal-type="3rd" onclick="filterTournamentsByMedal('3rd')">🥉 <span class="fw-semibold text-dark">{{ $awardCounts['3rd'] }}</span></a>
<a href="#goals" class="border bg-white rounded px-2 py-1 text-decoration-none" style="font-size: 1rem;" onclick="document.getElementById('goals-tab').click();">🎯 <span class="fw-semibold text-dark">{{ $activeGoalsCount + $completedGoalsCount }}</span></a>
<a href="#" class="border bg-white rounded px-2 py-1 text-decoration-none" style="font-size: 1rem;"> <span class="fw-semibold text-dark">{{ $totalAffiliations }}</span></a>
</div>
<!-- Status Badges -->
@ -990,8 +990,120 @@
<div class="tab-pane fade" id="affiliations" role="tabpanel">
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h5 class="fw-bold mb-3"><i class="bi bi-diagram-3 me-2"></i>Affiliations & Badges</h5>
<p class="text-muted">Affiliation system coming soon...</p>
<h5 class="fw-bold mb-3"><i class="bi bi-diagram-3 me-2"></i>Affiliations & Skills</h5>
@if($clubAffiliations->count() > 0)
<!-- Summary Stats -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card shadow-sm bg-primary text-white">
<div class="card-body text-center">
<i class="bi bi-building display-4 mb-2"></i>
<h4 class="fw-bold mb-1">{{ $totalAffiliations }}</h4>
<small>Total Affiliations</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm bg-success text-white">
<div class="card-body text-center">
<i class="bi bi-star display-4 mb-2"></i>
<h4 class="fw-bold mb-1">{{ $distinctSkills }}</h4>
<small>Distinct Skills</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm bg-info text-white">
<div class="card-body text-center">
<i class="bi bi-calendar-check display-4 mb-2"></i>
<h4 class="fw-bold mb-1">{{ floor($totalMembershipDuration / 12) }}y {{ $totalMembershipDuration % 12 }}m</h4>
<small>Total Membership</small>
</div>
</div>
</div>
</div>
<!-- Timeline and Skills Wheel Container -->
<div class="row">
<!-- Timeline -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h6 class="card-title mb-0"><i class="bi bi-timeline me-2"></i>Club Affiliations Timeline</h6>
</div>
<div class="card-body" style="max-height: 600px; overflow-y: auto;">
<div class="timeline">
@foreach($clubAffiliations as $index => $affiliation)
<div class="timeline-item mb-4 position-relative" data-affiliation-id="{{ $affiliation->id }}">
<div class="timeline-marker bg-primary"></div>
<div class="timeline-content card border {{ $index === 0 ? 'border-primary' : '' }} affiliation-card" style="cursor: pointer;" data-affiliation-id="{{ $affiliation->id }}">
<div class="card-body p-3">
<div class="d-flex align-items-center mb-2">
@if($affiliation->logo)
<img src="{{ asset('storage/' . $affiliation->logo) }}" alt="{{ $affiliation->club_name }}" class="me-3 rounded" style="width: 40px; height: 40px; object-fit: cover;">
@else
<div class="bg-primary text-white rounded d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-building"></i>
</div>
@endif
<div class="flex-grow-1">
<h6 class="mb-0 fw-bold">{{ $affiliation->club_name }}</h6>
<small class="text-muted">{{ $affiliation->date_range }}</small>
<br>
<span class="badge bg-info text-dark small">{{ $affiliation->formatted_duration }}</span>
</div>
</div>
@if($affiliation->location)
<div class="text-muted small mb-1">
<i class="bi bi-geo-alt me-1"></i>{{ $affiliation->location }}
</div>
@endif
<div class="text-muted small">
<i class="bi bi-star me-1"></i>{{ $affiliation->skillAcquisitions->count() }} skills acquired
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
<!-- Skills Wheel and Details -->
<div class="col-lg-6">
<!-- Skills Wheel -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h6 class="card-title mb-0"><i class="bi bi-pie-chart me-2"></i>Skills Wheel</h6>
</div>
<div class="card-body">
<div class="text-center">
<canvas id="skillsChart" width="300" height="300"></canvas>
<p id="noSkillsMessage" class="text-muted mt-3 d-none">Select an affiliation to view skills</p>
</div>
</div>
</div>
<!-- Affiliation Details -->
<div class="card shadow-sm">
<div class="card-header bg-light">
<h6 class="card-title mb-0"><i class="bi bi-info-circle me-2"></i>Affiliation Details</h6>
</div>
<div class="card-body" id="affiliationDetails">
<p class="text-muted">Select an affiliation from the timeline to view details</p>
</div>
</div>
</div>
</div>
@else
<div class="text-center py-5">
<i class="bi bi-diagram-3 text-muted" style="font-size: 3rem;"></i>
<h5 class="text-muted mt-3 mb-2">No Affiliations Yet</h5>
<p class="text-muted mb-0">Club affiliations and skills will appear here once added</p>
</div>
@endif
</div>
</div>
</div>
@ -1069,6 +1181,7 @@
<thead class="table-light">
<tr>
<th class="text-muted small fw-semibold">Tournament Details</th>
<th class="text-muted small fw-semibold">Club Affiliation</th>
<th class="text-muted small fw-semibold">Performance & Result</th>
<th class="text-muted small fw-semibold">Notes & Media</th>
</tr>
@ -1095,6 +1208,16 @@
@endif
</div>
</td>
<td>
@if($event->clubAffiliation)
<div>
<div class="small fw-semibold">{{ $event->clubAffiliation->club_name }}</div>
<div class="text-muted small">{{ $event->clubAffiliation->location }}</div>
</div>
@else
<span class="text-muted small">Individual</span>
@endif
</td>
<td>
@if($event->performanceResults->count() > 0)
@foreach($event->performanceResults as $result)
@ -1297,10 +1420,215 @@
</div>
</div>
<!-- Tournament Participation Modal -->
<div class="modal fade" id="tournamentParticipationModal" tabindex="-1" aria-labelledby="tournamentParticipationModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="tournamentParticipationModalLabel">Add Tournament Participation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="tournamentParticipationForm" method="POST" action="{{ route('family.store-tournament', $relationship->dependent->id) }}">
@csrf
<div class="modal-body">
<div class="row g-3">
<!-- Tournament Details -->
<div class="col-md-6">
<label for="tournament_title" class="form-label">Tournament Title <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="tournament_title" name="title" required>
</div>
<div class="col-md-6">
<label for="tournament_type" class="form-label">Type <span class="text-danger">*</span></label>
<select class="form-select" id="tournament_type" name="type" required>
<option value="">Select Type</option>
<option value="championship">Championship</option>
<option value="tournament">Tournament</option>
<option value="competition">Competition</option>
<option value="exhibition">Exhibition</option>
</select>
</div>
<div class="col-md-6">
<label for="tournament_sport" class="form-label">Sport <span class="text-danger">*</span></label>
<select class="form-select" id="tournament_sport" name="sport" required>
<option value="">Select Sport</option>
<option value="Boxing">Boxing</option>
<option value="Taekwondo">Taekwondo</option>
<option value="Karate">Karate</option>
<option value="Martial Arts">Martial Arts</option>
<option value="Fitness">Fitness</option>
<option value="Weightlifting">Weightlifting</option>
<option value="Other">Other</option>
</select>
</div>
<div class="col-md-6">
<label for="tournament_date" class="form-label">Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" id="tournament_date" name="date" required>
</div>
<div class="col-md-6">
<label for="tournament_time" class="form-label">Time</label>
<input type="time" class="form-control" id="tournament_time" name="time">
</div>
<div class="col-md-6">
<label for="tournament_location" class="form-label">Location</label>
<input type="text" class="form-control" id="tournament_location" name="location" placeholder="Venue name or address">
</div>
<div class="col-md-6">
<label for="participants_count" class="form-label">Number of Participants</label>
<input type="number" class="form-control" id="participants_count" name="participants_count" min="1">
</div>
<div class="col-md-6">
<label for="club_affiliation_id" class="form-label">Club Affiliation</label>
<select class="form-select" id="club_affiliation_id" name="club_affiliation_id">
<option value="">Select Club (Optional)</option>
@foreach($clubAffiliations ?? [] as $affiliation)
<option value="{{ $affiliation->id }}">{{ $affiliation->club_name }}</option>
@endforeach
</select>
</div>
<!-- Performance Results Section -->
<div class="col-12">
<hr>
<h6 class="mb-3">Performance Results</h6>
<div id="performanceResultsContainer">
<div class="performance-result-item mb-3 p-3 border rounded">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Medal Type</label>
<select class="form-select medal-type" name="performance_results[0][medal_type]">
<option value="">Select Medal</option>
<option value="special">Special Award</option>
<option value="1st">1st Place</option>
<option value="2nd">2nd Place</option>
<option value="3rd">3rd Place</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Points</label>
<input type="number" class="form-control" name="performance_results[0][points]" min="0" step="0.1">
</div>
<div class="col-md-4">
<label class="form-label">Description</label>
<input type="text" class="form-control" name="performance_results[0][description]" placeholder="Optional description">
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-result" style="display: none;">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-primary btn-sm" id="addPerformanceResult">
<i class="bi bi-plus me-1"></i>Add Another Result
</button>
</div>
<!-- Notes & Media Section -->
<div class="col-12">
<hr>
<h6 class="mb-3">Notes & Media</h6>
<div id="notesMediaContainer">
<div class="notes-media-item mb-3 p-3 border rounded">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Note Text</label>
<textarea class="form-control" name="notes_media[0][note_text]" rows="2" placeholder="Optional notes about the tournament"></textarea>
</div>
<div class="col-md-5">
<label class="form-label">Media Link</label>
<input type="url" class="form-control" name="notes_media[0][media_link]" placeholder="https://example.com/photo.jpg">
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-note" style="display: none;">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-primary btn-sm" id="addNotesMedia">
<i class="bi bi-plus me-1"></i>Add Another Note/Media
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Tournament Record</button>
</div>
</form>
</div>
</div>
</div>
<style>
.history-row:hover .edit-record-btn {
opacity: 1 !important;
}
/* Timeline Styles */
.timeline {
position: relative;
padding-left: 30px;
}
.timeline::before {
content: '';
position: absolute;
left: 15px;
top: 0;
bottom: 0;
width: 2px;
background: #e9ecef;
}
.timeline-item {
position: relative;
margin-bottom: 20px;
}
.timeline-marker {
position: absolute;
left: -22px;
top: 20px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #6c757d;
border: 2px solid #fff;
z-index: 1;
}
.timeline-marker.bg-primary {
background: #0d6efd;
}
.timeline-content {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.timeline-content:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
.affiliation-card {
transition: all 0.3s ease;
}
.affiliation-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
transform: translateY(-2px);
}
.affiliation-card.border-primary {
border-color: #0d6efd !important;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25) !important;
}
</style>
<script>
@ -1666,9 +1994,11 @@
const tournamentsTable = document.getElementById('tournamentsTable');
const awardCards = document.getElementById('awardCards');
if (sportFilter && tournamentsTable) {
sportFilter.addEventListener('change', function() {
const selectedSport = this.value;
// Global variables for current filters
let currentSportFilter = 'all';
let currentMedalFilter = 'all';
function applyTournamentFilters() {
const rows = tournamentsTable.querySelectorAll('tbody tr');
let visibleRows = 0;
@ -1676,12 +2006,34 @@
rows.forEach(row => {
const sport = row.getAttribute('data-sport');
if (selectedSport === 'all' || sport === selectedSport) {
const performanceCell = row.querySelector('td:nth-child(3)');
let hasMatchingMedal = false;
if (performanceCell) {
const badges = performanceCell.querySelectorAll('.badge');
badges.forEach(badge => {
if (currentMedalFilter === 'all') {
hasMatchingMedal = true;
} else if (currentMedalFilter === 'special' && badge.textContent.includes('Special Award')) {
hasMatchingMedal = true;
} else if (currentMedalFilter === '1st' && badge.textContent.includes('1st Place')) {
hasMatchingMedal = true;
} else if (currentMedalFilter === '2nd' && badge.textContent.includes('2nd Place')) {
hasMatchingMedal = true;
} else if (currentMedalFilter === '3rd' && badge.textContent.includes('3rd Place')) {
hasMatchingMedal = true;
}
});
}
const sportMatch = currentSportFilter === 'all' || sport === currentSportFilter;
const medalMatch = currentMedalFilter === 'all' || hasMatchingMedal;
if (sportMatch && medalMatch) {
row.style.display = '';
visibleRows++;
// Count awards in visible rows
const performanceCell = row.querySelector('td:nth-child(2)');
if (performanceCell) {
const badges = performanceCell.querySelectorAll('.badge');
badges.forEach(badge => {
@ -1708,9 +2060,37 @@
} else {
awardCards.style.display = '';
}
}
if (sportFilter && tournamentsTable) {
sportFilter.addEventListener('change', function() {
currentSportFilter = this.value;
applyTournamentFilters();
});
}
// Function to filter tournaments by medal type (called from achievement badges)
window.filterTournamentsByMedal = function(medalType) {
// Switch to tournaments tab
const tournamentsTab = document.getElementById('tournaments-tab');
if (tournamentsTab) {
const tab = new bootstrap.Tab(tournamentsTab);
tab.show();
}
// Set medal filter
currentMedalFilter = medalType;
currentSportFilter = 'all'; // Reset sport filter
// Reset sport filter dropdown
if (sportFilter) {
sportFilter.value = 'all';
}
// Apply filters
applyTournamentFilters();
};
// Goals filtering functionality
const goalFilterCards = document.querySelectorAll('.goal-filter-card');
const goalsContainer = document.querySelector('.row.g-4'); // Container with goal cards
@ -1870,4 +2250,389 @@
});
});
</script>
<!-- Chart.js for Skills Wheel -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Affiliations Tab JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Affiliations data
const affiliationsData = @json($clubAffiliations);
let skillsChart = null;
let selectedAffiliationId = null;
// Initialize affiliations functionality
function initAffiliations() {
// Set up timeline click handlers
document.querySelectorAll('.affiliation-card').forEach(card => {
card.addEventListener('click', function() {
const affiliationId = this.getAttribute('data-affiliation-id');
selectAffiliation(affiliationId);
// Update visual selection
document.querySelectorAll('.affiliation-card').forEach(c => {
c.classList.remove('border-primary');
});
this.classList.add('border-primary');
});
});
// Select first affiliation by default if available
if (affiliationsData.length > 0) {
selectAffiliation(affiliationsData[0].id);
}
}
function selectAffiliation(affiliationId) {
selectedAffiliationId = affiliationId;
const affiliation = affiliationsData.find(a => a.id == affiliationId);
if (!affiliation) return;
// Update skills chart
updateSkillsChart(affiliation.skill_acquisitions || []);
// Update affiliation details
updateAffiliationDetails(affiliation);
}
function updateSkillsChart(skills) {
const ctx = document.getElementById('skillsChart').getContext('2d');
const noSkillsMessage = document.getElementById('noSkillsMessage');
if (skills.length === 0) {
if (skillsChart) {
skillsChart.destroy();
skillsChart = null;
}
document.getElementById('skillsChart').style.display = 'none';
noSkillsMessage.classList.remove('d-none');
return;
}
document.getElementById('skillsChart').style.display = 'block';
noSkillsMessage.classList.add('d-none');
// Prepare data for polar area chart
const labels = skills.map(skill => skill.skill_name);
const data = skills.map(skill => skill.duration_months);
const backgroundColors = [
'rgba(54, 162, 235, 0.8)',
'rgba(255, 99, 132, 0.8)',
'rgba(255, 205, 86, 0.8)',
'rgba(75, 192, 192, 0.8)',
'rgba(153, 102, 255, 0.8)',
'rgba(255, 159, 64, 0.8)',
'rgba(199, 199, 199, 0.8)',
'rgba(83, 102, 255, 0.8)',
'rgba(255, 99, 255, 0.8)',
'rgba(99, 255, 132, 0.8)'
];
if (skillsChart) {
skillsChart.destroy();
}
skillsChart = new Chart(ctx, {
type: 'polarArea',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: backgroundColors.slice(0, skills.length),
borderWidth: 2,
borderColor: backgroundColors.slice(0, skills.length).map(color => color.replace('0.8', '1')),
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
padding: 20,
usePointStyle: true
}
},
tooltip: {
callbacks: {
label: function(context) {
const skill = skills[context.dataIndex];
const duration = skill.formatted_duration || `${skill.duration_months} months`;
return `${context.label}: ${duration}`;
}
}
}
},
scales: {
r: {
beginAtZero: true,
ticks: {
display: false
}
}
},
animation: {
animateScale: true,
animateRotate: true
}
}
});
}
function updateAffiliationDetails(affiliation) {
const detailsContainer = document.getElementById('affiliationDetails');
let html = `
<div class="d-flex align-items-center mb-3">
${affiliation.logo ?
`<img src="${affiliation.logo}" alt="${affiliation.club_name}" class="me-3 rounded" style="width: 50px; height: 50px; object-fit: cover;">` :
`<div class="bg-primary text-white rounded d-flex align-items-center justify-content-center me-3" style="width: 50px; height: 50px;">
<i class="bi bi-building"></i>
</div>`
}
<div>
<h5 class="mb-1">${affiliation.club_name}</h5>
<p class="text-muted mb-0">${affiliation.date_range}</p>
<span class="badge bg-info text-dark small">${affiliation.formatted_duration}</span>
</div>
</div>
`;
if (affiliation.location) {
html += `<p class="mb-2"><i class="bi bi-geo-alt me-2"></i><strong>Location:</strong> ${affiliation.location}</p>`;
}
if (affiliation.description) {
html += `<p class="mb-2"><strong>Description:</strong> ${affiliation.description}</p>`;
}
if (affiliation.coaches && affiliation.coaches.length > 0) {
html += `<p class="mb-2"><strong>Coaches:</strong> ${affiliation.coaches.join(', ')}</p>`;
}
if (affiliation.affiliation_media && affiliation.affiliation_media.length > 0) {
html += `<div class="mt-3"><strong>Media & Certificates:</strong></div>`;
html += `<div class="row g-2 mt-1">`;
affiliation.affiliation_media.forEach(media => {
const iconClass = media.icon_class || 'bi-file';
html += `
<div class="col-6">
<a href="${media.full_url}" target="_blank" class="btn btn-outline-secondary btn-sm w-100">
<i class="bi ${iconClass} me-1"></i>${media.title || media.media_type}
</a>
</div>
`;
});
html += `</div>`;
}
detailsContainer.innerHTML = html;
}
// Initialize when affiliations tab is shown
const affiliationsTab = document.getElementById('affiliations-tab');
if (affiliationsTab) {
affiliationsTab.addEventListener('shown.bs.tab', function() {
initAffiliations();
});
}
// Initialize immediately if affiliations tab is active
if (document.getElementById('affiliations').classList.contains('show')) {
initAffiliations();
}
});
// Tournament Participation Modal Functionality
document.addEventListener('DOMContentLoaded', function() {
let performanceResultIndex = 1;
let notesMediaIndex = 1;
// Add Performance Result
document.getElementById('addPerformanceResult').addEventListener('click', function() {
const container = document.getElementById('performanceResultsContainer');
const newItem = document.createElement('div');
newItem.className = 'performance-result-item mb-3 p-3 border rounded';
newItem.innerHTML = `
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Medal Type</label>
<select class="form-select medal-type" name="performance_results[${performanceResultIndex}][medal_type]">
<option value="">Select Medal</option>
<option value="special">Special Award</option>
<option value="1st">1st Place</option>
<option value="2nd">2nd Place</option>
<option value="3rd">3rd Place</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Points</label>
<input type="number" class="form-control" name="performance_results[${performanceResultIndex}][points]" min="0" step="0.1">
</div>
<div class="col-md-4">
<label class="form-label">Description</label>
<input type="text" class="form-control" name="performance_results[${performanceResultIndex}][description]" placeholder="Optional description">
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-result">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
container.appendChild(newItem);
performanceResultIndex++;
// Show remove buttons if more than one result
updateRemoveButtons('performance-result-item', 'remove-result');
});
// Add Notes & Media
document.getElementById('addNotesMedia').addEventListener('click', function() {
const container = document.getElementById('notesMediaContainer');
const newItem = document.createElement('div');
newItem.className = 'notes-media-item mb-3 p-3 border rounded';
newItem.innerHTML = `
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Note Text</label>
<textarea class="form-control" name="notes_media[${notesMediaIndex}][note_text]" rows="2" placeholder="Optional notes about the tournament"></textarea>
</div>
<div class="col-md-5">
<label class="form-label">Media Link</label>
<input type="url" class="form-control" name="notes_media[${notesMediaIndex}][media_link]" placeholder="https://example.com/photo.jpg">
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-note">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
container.appendChild(newItem);
notesMediaIndex++;
// Show remove buttons if more than one note
updateRemoveButtons('notes-media-item', 'remove-note');
});
// Remove Performance Result
document.addEventListener('click', function(e) {
if (e.target.closest('.remove-result')) {
e.target.closest('.performance-result-item').remove();
updateRemoveButtons('performance-result-item', 'remove-result');
}
});
// Remove Notes & Media
document.addEventListener('click', function(e) {
if (e.target.closest('.remove-note')) {
e.target.closest('.notes-media-item').remove();
updateRemoveButtons('notes-media-item', 'remove-note');
}
});
function updateRemoveButtons(itemClass, buttonClass) {
const items = document.querySelectorAll('.' + itemClass);
const buttons = document.querySelectorAll('.' + buttonClass);
if (items.length > 1) {
buttons.forEach(button => button.style.display = 'block');
} else {
buttons.forEach(button => button.style.display = 'none');
}
}
// Reset modal when opened
document.getElementById('tournamentParticipationModal').addEventListener('show.bs.modal', function() {
// Reset form
document.getElementById('tournamentParticipationForm').reset();
// Reset dynamic content
const performanceContainer = document.getElementById('performanceResultsContainer');
const notesContainer = document.getElementById('notesMediaContainer');
// Keep only the first item in each container
const performanceItems = performanceContainer.querySelectorAll('.performance-result-item');
const notesItems = notesContainer.querySelectorAll('.notes-media-item');
for (let i = 1; i < performanceItems.length; i++) {
performanceItems[i].remove();
}
for (let i = 1; i < notesItems.length; i++) {
notesItems[i].remove();
}
// Reset indices
performanceResultIndex = 1;
notesMediaIndex = 1;
// Hide remove buttons
document.querySelectorAll('.remove-result, .remove-note').forEach(button => {
button.style.display = 'none';
});
});
// Handle form submission
document.getElementById('tournamentParticipationForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('tournamentParticipationModal'));
modal.hide();
// Show success message
showAlert('Tournament record added successfully!', 'success');
// Reload page to show new data
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showAlert('Error adding tournament record: ' + (data.message || 'Unknown error'), 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('Error adding tournament record. Please try again.', 'danger');
});
});
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
document.body.appendChild(alertDiv);
// Auto remove after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
});
</script>
@endsection

View File

@ -95,6 +95,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::post('/family/{id}/health', [FamilyController::class, 'storeHealth'])->name('family.store-health');
Route::put('/family/{id}/health/{recordId}', [FamilyController::class, 'updateHealth'])->name('family.update-health');
Route::put('/family/goal/{goalId}', [FamilyController::class, 'updateGoal'])->name('family.update-goal');
Route::post('/family/{id}/tournament', [FamilyController::class, 'storeTournament'])->name('family.store-tournament');
Route::post('/family/{id}/upload-picture', [FamilyController::class, 'uploadFamilyMemberPicture'])->name('family.upload-picture');
Route::delete('/family/{id}', [FamilyController::class, 'destroy'])->name('family.destroy');