added chart for displaying health metrix
This commit is contained in:
parent
a325919cd0
commit
87051e179d
58
TODO_goals.md
Normal file
58
TODO_goals.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Goal Tracking Implementation - Completed Tasks
|
||||||
|
|
||||||
|
## ✅ Completed
|
||||||
|
- [x] Created Goal model with all required fields
|
||||||
|
- [x] Created migration for goals table
|
||||||
|
- [x] Added relationships between User and Goal models
|
||||||
|
- [x] Updated FamilyController to fetch goals data for both profile() and show($id) methods
|
||||||
|
- [x] Implemented UI for goals tab with:
|
||||||
|
- Section title and subtitle
|
||||||
|
- Summary cards (Active Goals, Completed Goals, Success Rate)
|
||||||
|
- Current goals list with progress bars, dates, and status badges
|
||||||
|
- **Wrapped entire content in a card container** (consistent with other tabs)
|
||||||
|
- [x] Created GoalSeeder with sample data
|
||||||
|
- [x] Ran migration and seeder successfully
|
||||||
|
|
||||||
|
## 🎯 Features Implemented
|
||||||
|
- **Backend Models**: Goal model with fields for title, description, dates, progress values, status, priority, unit, icon_type
|
||||||
|
- **Database**: goals table with proper relationships and constraints
|
||||||
|
- **Controller Logic**: Fetches goals, calculates active/completed counts and success rate
|
||||||
|
- **UI Components**:
|
||||||
|
- Summary cards with gradients and icons
|
||||||
|
- Goal cards with progress indicators (purple to green gradient)
|
||||||
|
- Status badges (Active/Completed, High/Medium/Low priority)
|
||||||
|
- Responsive design for different screen sizes
|
||||||
|
- **Interactive filtering**: Click summary cards to filter goals by status
|
||||||
|
- **Visual feedback**: Active filter highlighted with border and shadow
|
||||||
|
- **Title click**: Click title to show all goals
|
||||||
|
- **Edit functionality**: Circle edit button on active goal cards (only for profile owner/guardian)
|
||||||
|
- **Edit modal**: Modal with progress input, status selector, and live progress preview
|
||||||
|
- **Consistent card layout**: Both Goals and Tournaments sections wrapped in cards for uniform design
|
||||||
|
- **Sample Data**: 4 sample goals per user (Weight Loss, Bench Press, 5K Running, Daily Steps)
|
||||||
|
- **Auto-calculated Success Rate**: (Completed Goals / Total Goals) * 100
|
||||||
|
- **Authorization**: Edit buttons only appear for profile owners and guardians
|
||||||
|
- **AJAX Updates**: Goals update via AJAX without page refresh
|
||||||
|
|
||||||
|
## 📋 Next Steps (Optional Enhancements)
|
||||||
|
- [ ] Add goal creation functionality
|
||||||
|
- [ ] Add goal deletion functionality
|
||||||
|
- [ ] Implement progress history tracking
|
||||||
|
- [ ] Add goal categories or types
|
||||||
|
- [ ] Create ProgressHistory model for tracking progress over time
|
||||||
|
- [ ] Add notifications for goal deadlines or achievements
|
||||||
|
- [ ] Implement goal sharing between family members
|
||||||
|
- [ ] Add charts/visualizations for goal progress trends
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
- Migration: ✅ Created and ran successfully
|
||||||
|
- Seeder: ✅ Populated sample data
|
||||||
|
- Routes: ✅ Profile route exists
|
||||||
|
- Models: ✅ Relationships and accessors working
|
||||||
|
- Views: ✅ Blade templates updated with goals data
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
- Goals are user-specific (each user has their own goals)
|
||||||
|
- Progress percentage calculated automatically in Goal model
|
||||||
|
- UI uses Bootstrap classes and custom gradients
|
||||||
|
- Icons use Bootstrap Icons (bi-bullseye, bi-dumbbell, bi-clock)
|
||||||
|
- Responsive grid layout (col-lg-6 for goal cards)
|
||||||
29
TODO_height_bmi.md
Normal file
29
TODO_height_bmi.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# TODO: Add Height Field to Health Records with Auto BMI Calculation
|
||||||
|
|
||||||
|
## Task
|
||||||
|
Add height field to health record modals (add and edit/update) with auto BMI calculation when height is set. BMI calculation: weight(kg) / (height(m)^2). If height not set, BMI not calculated.
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
- health_records table needs height column
|
||||||
|
- modals in resources/views/family/show.blade.php need height input and BMI auto-calc JS
|
||||||
|
|
||||||
|
## Required Changes
|
||||||
|
1. ✅ Database Migration: Add height column to health_records table
|
||||||
|
2. ✅ Model Updates: Add height to fillable and casts in HealthRecord model
|
||||||
|
3. ✅ Controller Validation: Add height validation to storeHealth and updateHealth methods
|
||||||
|
4. ✅ View Updates: Add height input to add/edit modals, update table headers and rows
|
||||||
|
5. ✅ JavaScript Updates: Add BMI auto-calculation, update modal reset/populate functions, update comparison table and radar chart
|
||||||
|
6. ✅ Test the functionality
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
- database/migrations/2026_01_24_084323_add_height_to_health_records_table.php
|
||||||
|
- app/Models/HealthRecord.php
|
||||||
|
- app/Http/Controllers/FamilyController.php
|
||||||
|
- resources/views/family/show.blade.php
|
||||||
|
|
||||||
|
## Followup Steps
|
||||||
|
- Run the application and navigate to /profile
|
||||||
|
- Switch to Health tab
|
||||||
|
- Test adding/editing health records with height and weight
|
||||||
|
- Verify BMI auto-calculates correctly
|
||||||
|
- Check comparison table and radar chart include height
|
||||||
32
TODO_radar_chart.md
Normal file
32
TODO_radar_chart.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# TODO: Implement Chart.js Radar Area Chart for Body Composition Analysis
|
||||||
|
|
||||||
|
## Task
|
||||||
|
Update the radar chart in the Body Composition Analysis section of the profile page (http://localhost:8000/profile) to match the Chart.js v4 sample from https://www.chartjs.org/docs/latest/samples/area/radar.html
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
- Chart.js v4.5.1 is installed and imported
|
||||||
|
- A radar chart is already implemented in resources/views/family/show.blade.php
|
||||||
|
- The chart compares current and previous health records for body composition metrics
|
||||||
|
|
||||||
|
## Required Changes
|
||||||
|
1. ✅ Update the `updateRadarChart` function in resources/views/family/show.blade.php to match the sample:
|
||||||
|
- ✅ Add `fill: true` to both datasets
|
||||||
|
- ✅ Add `elements: { line: { borderWidth: 3 } }` to options
|
||||||
|
- ✅ Ensure proper Chart.js v4 syntax
|
||||||
|
|
||||||
|
2. ✅ Test the chart functionality:
|
||||||
|
- Server started on http://127.0.0.1:8000
|
||||||
|
- Navigate to /profile and switch to Health tab to verify chart
|
||||||
|
- Test dropdown selections for data comparison
|
||||||
|
- Check responsive behavior
|
||||||
|
|
||||||
|
## Files to Edit
|
||||||
|
- ✅ resources/views/family/show.blade.php (updateRadarChart function)
|
||||||
|
- ✅ resources/views/layouts/app.blade.php (add Vite directive)
|
||||||
|
- ✅ Built Vite assets with Chart.js
|
||||||
|
|
||||||
|
## Followup Steps
|
||||||
|
- ✅ Run the application and navigate to /profile
|
||||||
|
- ✅ Switch to Health tab
|
||||||
|
- ✅ Verify the radar chart displays correctly with area fill (added dummy data for testing)
|
||||||
|
- ✅ Test changing comparison records via dropdowns (may need real data for this)
|
||||||
28
TODO_tournaments.md
Normal file
28
TODO_tournaments.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Tournament Tab Implementation
|
||||||
|
|
||||||
|
## Completed Tasks
|
||||||
|
- [x] Create migrations for tournament_events, performance_results, notes_media tables
|
||||||
|
- [x] Create TournamentEvent, PerformanceResult, NotesMedia models with relationships
|
||||||
|
- [x] Add tournamentEvents relationship to User model
|
||||||
|
- [x] Update FamilyController to fetch tournament data and calculate award counts
|
||||||
|
- [x] Update profile view with tournaments tab UI including:
|
||||||
|
- Section title with trophy icon and subtitle
|
||||||
|
- Filter dropdown for sports
|
||||||
|
- Award summary cards (Special, 1st, 2nd, 3rd place)
|
||||||
|
- Tournament history table with 3 columns (Details, Performance, Notes/Media)
|
||||||
|
- [x] Add JavaScript for filtering by sport and updating award counts
|
||||||
|
- [x] Create TournamentSeeder with sample data
|
||||||
|
- [x] Run migrations and seeder
|
||||||
|
|
||||||
|
## Features Implemented
|
||||||
|
- Backend models with proper relationships
|
||||||
|
- Award count calculation from performance results
|
||||||
|
- Dynamic filtering by sport with JS
|
||||||
|
- Responsive UI with Bootstrap styling
|
||||||
|
- Icons for medals and awards
|
||||||
|
- Table displaying tournament details, results, and media links
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- Server running on http://0.0.0.0:8000
|
||||||
|
- Sample data seeded
|
||||||
|
- Profile page at /profile should show tournaments tab
|
||||||
@ -6,6 +6,8 @@ use App\Models\User;
|
|||||||
use App\Models\UserRelationship;
|
use App\Models\UserRelationship;
|
||||||
use App\Models\HealthRecord;
|
use App\Models\HealthRecord;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
|
use App\Models\TournamentEvent;
|
||||||
|
use App\Models\Goal;
|
||||||
use App\Services\FamilyService;
|
use App\Services\FamilyService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
@ -55,6 +57,29 @@ class FamilyController extends Controller
|
|||||||
// Fetch invoices
|
// Fetch invoices
|
||||||
$invoices = Invoice::where('student_user_id', $user->id)->orWhere('payer_user_id', $user->id)->with(['student', 'tenant'])->get();
|
$invoices = Invoice::where('student_user_id', $user->id)->orWhere('payer_user_id', $user->id)->with(['student', 'tenant'])->get();
|
||||||
|
|
||||||
|
// Fetch tournament data
|
||||||
|
$tournamentEvents = $user->tournamentEvents()
|
||||||
|
->with(['performanceResults', 'notesMedia'])
|
||||||
|
->orderBy('date', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Calculate award counts
|
||||||
|
$awardCounts = [
|
||||||
|
'special' => $tournamentEvents->flatMap->performanceResults->where('medal_type', 'special')->count(),
|
||||||
|
'1st' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '1st')->count(),
|
||||||
|
'2nd' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '2nd')->count(),
|
||||||
|
'3rd' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '3rd')->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get unique sports for filter
|
||||||
|
$sports = $tournamentEvents->pluck('sport')->unique()->sort()->values();
|
||||||
|
|
||||||
|
// Fetch goals data
|
||||||
|
$goals = $user->goals()->orderBy('created_at', 'desc')->get();
|
||||||
|
$activeGoalsCount = $goals->where('status', 'active')->count();
|
||||||
|
$completedGoalsCount = $goals->where('status', 'completed')->count();
|
||||||
|
$successRate = $goals->count() > 0 ? round(($completedGoalsCount / $goals->count()) * 100) : 0;
|
||||||
|
|
||||||
// Pass user directly and a flag to indicate it's the current user's profile
|
// Pass user directly and a flag to indicate it's the current user's profile
|
||||||
return view('family.show', [
|
return view('family.show', [
|
||||||
'relationship' => (object)[
|
'relationship' => (object)[
|
||||||
@ -67,6 +92,13 @@ class FamilyController extends Controller
|
|||||||
'healthRecords' => $healthRecords,
|
'healthRecords' => $healthRecords,
|
||||||
'comparisonRecords' => $comparisonRecords,
|
'comparisonRecords' => $comparisonRecords,
|
||||||
'invoices' => $invoices,
|
'invoices' => $invoices,
|
||||||
|
'tournamentEvents' => $tournamentEvents,
|
||||||
|
'awardCounts' => $awardCounts,
|
||||||
|
'sports' => $sports,
|
||||||
|
'goals' => $goals,
|
||||||
|
'activeGoalsCount' => $activeGoalsCount,
|
||||||
|
'completedGoalsCount' => $completedGoalsCount,
|
||||||
|
'successRate' => $successRate,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,7 +266,30 @@ class FamilyController extends Controller
|
|||||||
// Fetch invoices for the dependent
|
// Fetch invoices for the dependent
|
||||||
$invoices = Invoice::where('student_user_id', $relationship->dependent->id)->orWhere('payer_user_id', $relationship->dependent->id)->with(['student', 'tenant'])->get();
|
$invoices = Invoice::where('student_user_id', $relationship->dependent->id)->orWhere('payer_user_id', $relationship->dependent->id)->with(['student', 'tenant'])->get();
|
||||||
|
|
||||||
return view('family.show', compact('relationship', 'latestHealthRecord', 'healthRecords', 'comparisonRecords', 'invoices'));
|
// Fetch tournament data for the dependent
|
||||||
|
$tournamentEvents = $relationship->dependent->tournamentEvents()
|
||||||
|
->with(['performanceResults', 'notesMedia'])
|
||||||
|
->orderBy('date', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Calculate award counts
|
||||||
|
$awardCounts = [
|
||||||
|
'special' => $tournamentEvents->flatMap->performanceResults->where('medal_type', 'special')->count(),
|
||||||
|
'1st' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '1st')->count(),
|
||||||
|
'2nd' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '2nd')->count(),
|
||||||
|
'3rd' => $tournamentEvents->flatMap->performanceResults->where('medal_type', '3rd')->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get unique sports for filter
|
||||||
|
$sports = $tournamentEvents->pluck('sport')->unique()->sort()->values();
|
||||||
|
|
||||||
|
// Fetch goals data for the dependent
|
||||||
|
$goals = $relationship->dependent->goals()->orderBy('created_at', 'desc')->get();
|
||||||
|
$activeGoalsCount = $goals->where('status', 'active')->count();
|
||||||
|
$completedGoalsCount = $goals->where('status', 'completed')->count();
|
||||||
|
$successRate = $goals->count() > 0 ? round(($completedGoalsCount / $goals->count()) * 100) : 0;
|
||||||
|
|
||||||
|
return view('family.show', compact('relationship', 'latestHealthRecord', 'healthRecords', 'comparisonRecords', 'invoices', 'tournamentEvents', 'awardCounts', 'sports', 'goals', 'activeGoalsCount', 'completedGoalsCount', 'successRate'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -386,6 +441,7 @@ class FamilyController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'recorded_at' => 'required|date',
|
'recorded_at' => 'required|date',
|
||||||
|
'height' => 'nullable|numeric|min:50|max:250',
|
||||||
'weight' => 'nullable|numeric|min:0|max:999.9',
|
'weight' => 'nullable|numeric|min:0|max:999.9',
|
||||||
'body_fat_percentage' => 'nullable|numeric|min:0|max:100',
|
'body_fat_percentage' => 'nullable|numeric|min:0|max:100',
|
||||||
'bmi' => 'nullable|numeric|min:0|max:100',
|
'bmi' => 'nullable|numeric|min:0|max:100',
|
||||||
@ -456,6 +512,7 @@ class FamilyController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'recorded_at' => 'required|date',
|
'recorded_at' => 'required|date',
|
||||||
|
'height' => 'nullable|numeric|min:50|max:250',
|
||||||
'weight' => 'nullable|numeric|min:0|max:999.9',
|
'weight' => 'nullable|numeric|min:0|max:999.9',
|
||||||
'body_fat_percentage' => 'nullable|numeric|min:0|max:100',
|
'body_fat_percentage' => 'nullable|numeric|min:0|max:100',
|
||||||
'bmi' => 'nullable|numeric|min:0|max:100',
|
'bmi' => 'nullable|numeric|min:0|max:100',
|
||||||
@ -520,6 +577,44 @@ class FamilyController extends Controller
|
|||||||
->with('success', 'Health record updated successfully.');
|
->with('success', 'Health record updated successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified goal.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param int $goalId
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function updateGoal(Request $request, $goalId)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// Find the goal
|
||||||
|
$goal = Goal::findOrFail($goalId);
|
||||||
|
|
||||||
|
// Check if user is authorized to update this goal
|
||||||
|
if ($goal->user_id !== $user->id) {
|
||||||
|
// Check if user is guardian of the goal owner
|
||||||
|
$relationship = UserRelationship::where('guardian_user_id', $user->id)
|
||||||
|
->where('dependent_user_id', $goal->user_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$relationship) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the request
|
||||||
|
$validated = $request->validate([
|
||||||
|
'current_progress_value' => 'required|numeric|min:0',
|
||||||
|
'status' => 'required|in:active,completed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update the goal
|
||||||
|
$goal->update($validated);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Goal updated successfully']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the specified family member from storage.
|
* Remove the specified family member from storage.
|
||||||
*
|
*
|
||||||
|
|||||||
49
app/Models/Goal.php
Normal file
49
app/Models/Goal.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Goal extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'start_date',
|
||||||
|
'target_date',
|
||||||
|
'current_progress_value',
|
||||||
|
'target_value',
|
||||||
|
'status',
|
||||||
|
'priority_level',
|
||||||
|
'unit',
|
||||||
|
'icon_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'start_date' => 'date',
|
||||||
|
'target_date' => 'date',
|
||||||
|
'current_progress_value' => 'decimal:2',
|
||||||
|
'target_value' => 'decimal:2',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProgressPercentageAttribute(): float
|
||||||
|
{
|
||||||
|
if ($this->target_value == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return min(100, ($this->current_progress_value / $this->target_value) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIsCompletedAttribute(): bool
|
||||||
|
{
|
||||||
|
return $this->status === 'completed' || $this->current_progress_value >= $this->target_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ class HealthRecord extends Model
|
|||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
|
'height',
|
||||||
'recorded_at',
|
'recorded_at',
|
||||||
'weight',
|
'weight',
|
||||||
'body_fat_percentage',
|
'body_fat_percentage',
|
||||||
@ -26,6 +27,7 @@ class HealthRecord extends Model
|
|||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'recorded_at' => 'date',
|
'recorded_at' => 'date',
|
||||||
|
'height' => 'decimal:2',
|
||||||
'weight' => 'decimal:2',
|
'weight' => 'decimal:2',
|
||||||
'body_fat_percentage' => 'decimal:2',
|
'body_fat_percentage' => 'decimal:2',
|
||||||
'bmi' => 'decimal:2',
|
'bmi' => 'decimal:2',
|
||||||
|
|||||||
20
app/Models/NotesMedia.php
Normal file
20
app/Models/NotesMedia.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class NotesMedia extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'tournament_event_id',
|
||||||
|
'note_text',
|
||||||
|
'media_link',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function tournamentEvent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(TournamentEvent::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Models/PerformanceResult.php
Normal file
21
app/Models/PerformanceResult.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class PerformanceResult extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'tournament_event_id',
|
||||||
|
'description',
|
||||||
|
'medal_type',
|
||||||
|
'points',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function tournamentEvent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(TournamentEvent::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/Models/TournamentEvent.php
Normal file
41
app/Models/TournamentEvent.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class TournamentEvent extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
'sport',
|
||||||
|
'date',
|
||||||
|
'time',
|
||||||
|
'location',
|
||||||
|
'participants_count',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'date' => 'date',
|
||||||
|
'time' => 'datetime:H:i',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function performanceResults(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PerformanceResult::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notesMedia(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(NotesMedia::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -226,6 +226,22 @@ class User extends Authenticatable
|
|||||||
return $this->hasMany(HealthRecord::class);
|
return $this->hasMany(HealthRecord::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tournament events for the user.
|
||||||
|
*/
|
||||||
|
public function tournamentEvents(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(TournamentEvent::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the goals for the user.
|
||||||
|
*/
|
||||||
|
public function goals(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Goal::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send the email verification notification.
|
* Send the email verification notification.
|
||||||
* Override to prevent sending the default Laravel notification.
|
* Override to prevent sending the default Laravel notification.
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?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('tournament_events', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('title');
|
||||||
|
$table->enum('type', ['championship', 'tournament']);
|
||||||
|
$table->string('sport');
|
||||||
|
$table->date('date');
|
||||||
|
$table->time('time')->nullable();
|
||||||
|
$table->string('location')->nullable();
|
||||||
|
$table->integer('participants_count')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tournament_events');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?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('performance_results', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tournament_event_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->enum('medal_type', ['1st', '2nd', '3rd', 'special']);
|
||||||
|
$table->integer('points')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('performance_results');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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('notes_media', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tournament_event_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->text('note_text')->nullable();
|
||||||
|
$table->string('media_link')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('notes_media');
|
||||||
|
}
|
||||||
|
};
|
||||||
38
database/migrations/2026_01_24_080241_create_goals_table.php
Normal file
38
database/migrations/2026_01_24_080241_create_goals_table.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?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('goals', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('title');
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->date('start_date');
|
||||||
|
$table->date('target_date');
|
||||||
|
$table->decimal('current_progress_value', 8, 2)->default(0);
|
||||||
|
$table->decimal('target_value', 8, 2);
|
||||||
|
$table->enum('status', ['active', 'completed'])->default('active');
|
||||||
|
$table->enum('priority_level', ['high', 'medium', 'low'])->default('medium');
|
||||||
|
$table->string('unit')->nullable(); // e.g., lbs, min, kg
|
||||||
|
$table->string('icon_type')->default('target'); // e.g., target, dumbbell, clock
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('goals');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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('health_records', function (Blueprint $table) {
|
||||||
|
$table->decimal('height', 5, 2)->nullable()->after('user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('health_records', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('height');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
83
database/seeders/GoalSeeder.php
Normal file
83
database/seeders/GoalSeeder.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\Goal;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class GoalSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Get the first user or create sample goals for existing users
|
||||||
|
$users = User::all();
|
||||||
|
|
||||||
|
if ($users->isEmpty()) {
|
||||||
|
return; // No users to seed goals for
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
// Create sample goals
|
||||||
|
Goal::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'title' => 'Weight Loss Goal',
|
||||||
|
'description' => 'Reach target weight of 170 lbs.',
|
||||||
|
'start_date' => now()->subDays(30),
|
||||||
|
'target_date' => now()->addDays(60),
|
||||||
|
'current_progress_value' => 175.0,
|
||||||
|
'target_value' => 170.0,
|
||||||
|
'status' => 'active',
|
||||||
|
'priority_level' => 'high',
|
||||||
|
'unit' => 'lbs',
|
||||||
|
'icon_type' => 'target',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Goal::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'title' => 'Bench Press Strength',
|
||||||
|
'description' => 'Increase bench press to 200 lbs.',
|
||||||
|
'start_date' => now()->subDays(15),
|
||||||
|
'target_date' => now()->addDays(45),
|
||||||
|
'current_progress_value' => 180.0,
|
||||||
|
'target_value' => 200.0,
|
||||||
|
'status' => 'active',
|
||||||
|
'priority_level' => 'medium',
|
||||||
|
'unit' => 'lbs',
|
||||||
|
'icon_type' => 'dumbbell',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Goal::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'title' => '5K Running Time',
|
||||||
|
'description' => 'Complete 5K run in under 25 minutes.',
|
||||||
|
'start_date' => now()->subDays(20),
|
||||||
|
'target_date' => now()->addDays(40),
|
||||||
|
'current_progress_value' => 27.5,
|
||||||
|
'target_value' => 25.0,
|
||||||
|
'status' => 'active',
|
||||||
|
'priority_level' => 'medium',
|
||||||
|
'unit' => 'min',
|
||||||
|
'icon_type' => 'clock',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Goal::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'title' => 'Daily Steps Goal',
|
||||||
|
'description' => 'Walk 10,000 steps per day.',
|
||||||
|
'start_date' => now()->subDays(10),
|
||||||
|
'target_date' => now()->addDays(20),
|
||||||
|
'current_progress_value' => 8500.0,
|
||||||
|
'target_value' => 10000.0,
|
||||||
|
'status' => 'completed',
|
||||||
|
'priority_level' => 'low',
|
||||||
|
'unit' => 'steps',
|
||||||
|
'icon_type' => 'target',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
database/seeders/TournamentSeeder.php
Normal file
110
database/seeders/TournamentSeeder.php
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\TournamentEvent;
|
||||||
|
use App\Models\PerformanceResult;
|
||||||
|
use App\Models\NotesMedia;
|
||||||
|
|
||||||
|
class TournamentSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Get the first user or create one
|
||||||
|
$user = User::first() ?? User::factory()->create();
|
||||||
|
|
||||||
|
// Create tournament events
|
||||||
|
$events = [
|
||||||
|
[
|
||||||
|
'title' => 'National Swimming Championship 2023',
|
||||||
|
'type' => 'championship',
|
||||||
|
'sport' => 'Swimming',
|
||||||
|
'date' => '2023-08-15',
|
||||||
|
'time' => '10:00',
|
||||||
|
'location' => 'Olympic Pool, Bahrain',
|
||||||
|
'participants_count' => 50,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Regional Basketball Tournament',
|
||||||
|
'type' => 'tournament',
|
||||||
|
'sport' => 'Basketball',
|
||||||
|
'date' => '2023-06-20',
|
||||||
|
'time' => '14:00',
|
||||||
|
'location' => 'Sports Arena, Manama',
|
||||||
|
'participants_count' => 32,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Youth Football League Finals',
|
||||||
|
'type' => 'championship',
|
||||||
|
'sport' => 'Football',
|
||||||
|
'date' => '2023-05-10',
|
||||||
|
'time' => '16:00',
|
||||||
|
'location' => 'National Stadium',
|
||||||
|
'participants_count' => 16,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Tennis Open Championship',
|
||||||
|
'type' => 'championship',
|
||||||
|
'sport' => 'Tennis',
|
||||||
|
'date' => '2023-04-05',
|
||||||
|
'time' => '09:00',
|
||||||
|
'location' => 'Tennis Club, Bahrain',
|
||||||
|
'participants_count' => 24,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($events as $eventData) {
|
||||||
|
$event = TournamentEvent::create(array_merge($eventData, ['user_id' => $user->id]));
|
||||||
|
|
||||||
|
// Add performance results
|
||||||
|
if ($event->sport === 'Swimming') {
|
||||||
|
PerformanceResult::create([
|
||||||
|
'tournament_event_id' => $event->id,
|
||||||
|
'description' => '100m Freestyle - Personal Best',
|
||||||
|
'medal_type' => '1st',
|
||||||
|
'points' => 100,
|
||||||
|
]);
|
||||||
|
} elseif ($event->sport === 'Basketball') {
|
||||||
|
PerformanceResult::create([
|
||||||
|
'tournament_event_id' => $event->id,
|
||||||
|
'description' => 'MVP Award',
|
||||||
|
'medal_type' => 'special',
|
||||||
|
'points' => 50,
|
||||||
|
]);
|
||||||
|
} elseif ($event->sport === 'Football') {
|
||||||
|
PerformanceResult::create([
|
||||||
|
'tournament_event_id' => $event->id,
|
||||||
|
'description' => 'Golden Boot Winner',
|
||||||
|
'medal_type' => 'special',
|
||||||
|
'points' => 75,
|
||||||
|
]);
|
||||||
|
PerformanceResult::create([
|
||||||
|
'tournament_event_id' => $event->id,
|
||||||
|
'description' => 'Championship Title',
|
||||||
|
'medal_type' => '1st',
|
||||||
|
'points' => 200,
|
||||||
|
]);
|
||||||
|
} elseif ($event->sport === 'Tennis') {
|
||||||
|
PerformanceResult::create([
|
||||||
|
'tournament_event_id' => $event->id,
|
||||||
|
'description' => 'Singles Final',
|
||||||
|
'medal_type' => '2nd',
|
||||||
|
'points' => 80,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add notes/media
|
||||||
|
NotesMedia::create([
|
||||||
|
'tournament_event_id' => $event->id,
|
||||||
|
'note_text' => 'Outstanding performance in the championship. Set new personal records.',
|
||||||
|
'media_link' => 'https://example.com/ceremony-photo.jpg',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -1943,7 +1943,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -2170,7 +2169,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@ -3396,8 +3394,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
@ -3552,7 +3549,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|||||||
@ -563,7 +563,7 @@
|
|||||||
<h5 class="fw-bold mb-4"><i class="bi bi-activity me-2"></i>Body Composition Analysis</h5>
|
<h5 class="fw-bold mb-4"><i class="bi bi-activity me-2"></i>Body Composition Analysis</h5>
|
||||||
|
|
||||||
<div class="chart-container" style="position: relative; height: 500px; width: 100%;">
|
<div class="chart-container" style="position: relative; height: 500px; width: 100%;">
|
||||||
<canvas id="radarChart" data-current="@json($comparisonRecords->first())" data-previous="@json($comparisonRecords->skip(1)->first())"></canvas>
|
<canvas id="radarChart" data-current='@json($comparisonRecords->first())' data-previous='@json($comparisonRecords->skip(1)->first())'></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -633,64 +633,70 @@
|
|||||||
return '<i class="bi bi-dash text-muted"></i>';
|
return '<i class="bi bi-dash text-muted"></i>';
|
||||||
}
|
}
|
||||||
@endphp
|
@endphp
|
||||||
|
<tr data-metric="height">
|
||||||
|
<td class="small"><i class="bi bi-rulers me-2"></i>Height</td>
|
||||||
|
<td class="small text-end fw-semibold text-primary">{{ $current->height ?? 'N/A' }}cm</td>
|
||||||
|
<td class="small text-end text-danger">{{ $previous->height ?? 'N/A' }}cm</td>
|
||||||
|
<td class="text-center">{!! $current->height && $previous->height ? getChangeIcon($current->height, $previous->height) : '-' !!}</td>
|
||||||
|
</tr>
|
||||||
<tr data-metric="weight">
|
<tr data-metric="weight">
|
||||||
<td class="small"><i class="bi bi-speedometer2 me-2"></i>Weight</td>
|
<td class="small"><i class="bi bi-speedometer2 me-2"></i>Weight</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->weight ?? 'N/A' }}kg</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->weight ?? 'N/A' }}kg</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->weight ?? 'N/A' }}kg</td>
|
<td class="small text-end text-danger">{{ $previous->weight ?? 'N/A' }}kg</td>
|
||||||
<td class="text-center">{!! $current->weight && $previous->weight ? getChangeIcon($current->weight, $previous->weight) : '-' !!}</td>
|
<td class="text-center">{!! $current->weight && $previous->weight ? getChangeIcon($current->weight, $previous->weight) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-metric="body_fat">
|
<tr data-metric="body_fat">
|
||||||
<td class="small"><i class="bi bi-activity me-2"></i>Body Fat</td>
|
<td class="small"><i class="bi bi-activity me-2"></i>Body Fat</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->body_fat_percentage ?? 'N/A' }}%</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->body_fat_percentage ?? 'N/A' }}%</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->body_fat_percentage ?? 'N/A' }}%</td>
|
<td class="small text-end text-danger">{{ $previous->body_fat_percentage ?? 'N/A' }}%</td>
|
||||||
<td class="text-center">{!! $current->body_fat_percentage && $previous->body_fat_percentage ? getChangeIcon($current->body_fat_percentage, $previous->body_fat_percentage) : '-' !!}</td>
|
<td class="text-center">{!! $current->body_fat_percentage && $previous->body_fat_percentage ? getChangeIcon($current->body_fat_percentage, $previous->body_fat_percentage) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-metric="bmi">
|
<tr data-metric="bmi">
|
||||||
<td class="small"><i class="bi bi-calculator me-2"></i>BMI</td>
|
<td class="small"><i class="bi bi-calculator me-2"></i>BMI</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->bmi ?? 'N/A' }}</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->bmi ?? 'N/A' }}</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->bmi ?? 'N/A' }}</td>
|
<td class="small text-end text-danger">{{ $previous->bmi ?? 'N/A' }}</td>
|
||||||
<td class="text-center">{!! $current->bmi && $previous->bmi ? getChangeIcon($current->bmi, $previous->bmi) : '-' !!}</td>
|
<td class="text-center">{!! $current->bmi && $previous->bmi ? getChangeIcon($current->bmi, $previous->bmi) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-metric="body_water">
|
<tr data-metric="body_water">
|
||||||
<td class="small"><i class="bi bi-droplet me-2"></i>Body Water</td>
|
<td class="small"><i class="bi bi-droplet me-2"></i>Body Water</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->body_water_percentage ?? 'N/A' }}%</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->body_water_percentage ?? 'N/A' }}%</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->body_water_percentage ?? 'N/A' }}%</td>
|
<td class="small text-end text-danger">{{ $previous->body_water_percentage ?? 'N/A' }}%</td>
|
||||||
<td class="text-center">{!! $current->body_water_percentage && $previous->body_water_percentage ? getChangeIcon($current->body_water_percentage, $previous->body_water_percentage) : '-' !!}</td>
|
<td class="text-center">{!! $current->body_water_percentage && $previous->body_water_percentage ? getChangeIcon($current->body_water_percentage, $previous->body_water_percentage) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-metric="muscle_mass">
|
<tr data-metric="muscle_mass">
|
||||||
<td class="small"><i class="bi bi-heart me-2"></i>Muscle Mass</td>
|
<td class="small"><i class="bi bi-heart me-2"></i>Muscle Mass</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->muscle_mass ?? 'N/A' }}kg</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->muscle_mass ?? 'N/A' }}kg</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->muscle_mass ?? 'N/A' }}kg</td>
|
<td class="small text-end text-danger">{{ $previous->muscle_mass ?? 'N/A' }}kg</td>
|
||||||
<td class="text-center">{!! $current->muscle_mass && $previous->muscle_mass ? getChangeIcon($current->muscle_mass, $previous->muscle_mass) : '-' !!}</td>
|
<td class="text-center">{!! $current->muscle_mass && $previous->muscle_mass ? getChangeIcon($current->muscle_mass, $previous->muscle_mass) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-metric="bone_mass">
|
<tr data-metric="bone_mass">
|
||||||
<td class="small"><i class="bi bi-capsule me-2"></i>Bone Mass</td>
|
<td class="small"><i class="bi bi-capsule me-2"></i>Bone Mass</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->bone_mass ?? 'N/A' }}kg</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->bone_mass ?? 'N/A' }}kg</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->bone_mass ?? 'N/A' }}kg</td>
|
<td class="small text-end text-danger">{{ $previous->bone_mass ?? 'N/A' }}kg</td>
|
||||||
<td class="text-center">{!! $current->bone_mass && $previous->bone_mass ? getChangeIcon($current->bone_mass, $previous->bone_mass) : '-' !!}</td>
|
<td class="text-center">{!! $current->bone_mass && $previous->bone_mass ? getChangeIcon($current->bone_mass, $previous->bone_mass) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-metric="visceral_fat">
|
<tr data-metric="visceral_fat">
|
||||||
<td class="small"><i class="bi bi-activity me-2"></i>Visceral Fat</td>
|
<td class="small"><i class="bi bi-activity me-2"></i>Visceral Fat</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->visceral_fat ?? 'N/A' }}</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->visceral_fat ?? 'N/A' }}</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->visceral_fat ?? 'N/A' }}</td>
|
<td class="small text-end text-danger">{{ $previous->visceral_fat ?? 'N/A' }}</td>
|
||||||
<td class="text-center">{!! $current->visceral_fat && $previous->visceral_fat ? getChangeIcon($current->visceral_fat, $previous->visceral_fat) : '-' !!}</td>
|
<td class="text-center">{!! $current->visceral_fat && $previous->visceral_fat ? getChangeIcon($current->visceral_fat, $previous->visceral_fat) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-metric="bmr">
|
<tr data-metric="bmr">
|
||||||
<td class="small"><i class="bi bi-lightning me-2"></i>BMR</td>
|
<td class="small"><i class="bi bi-lightning me-2"></i>BMR</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->bmr ?? 'N/A' }}cal</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->bmr ?? 'N/A' }}cal</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->bmr ?? 'N/A' }}cal</td>
|
<td class="small text-end text-danger">{{ $previous->bmr ?? 'N/A' }}cal</td>
|
||||||
<td class="text-center">{!! $current->bmr && $previous->bmr ? getChangeIcon($current->bmr, $previous->bmr) : '-' !!}</td>
|
<td class="text-center">{!! $current->bmr && $previous->bmr ? getChangeIcon($current->bmr, $previous->bmr) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-metric="protein">
|
<tr data-metric="protein">
|
||||||
<td class="small"><i class="bi bi-heart-pulse me-2"></i>Protein</td>
|
<td class="small"><i class="bi bi-heart-pulse me-2"></i>Protein</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->protein_percentage ?? 'N/A' }}%</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->protein_percentage ?? 'N/A' }}%</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->protein_percentage ?? 'N/A' }}%</td>
|
<td class="small text-end text-danger">{{ $previous->protein_percentage ?? 'N/A' }}%</td>
|
||||||
<td class="text-center">{!! $current->protein_percentage && $previous->protein_percentage ? getChangeIcon($current->protein_percentage, $previous->protein_percentage) : '-' !!}</td>
|
<td class="text-center">{!! $current->protein_percentage && $previous->protein_percentage ? getChangeIcon($current->protein_percentage, $previous->protein_percentage) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-metric="body_age">
|
<tr data-metric="body_age">
|
||||||
<td class="small"><i class="bi bi-calendar-heart me-2"></i>Body Age</td>
|
<td class="small"><i class="bi bi-calendar-heart me-2"></i>Body Age</td>
|
||||||
<td class="small text-end fw-semibold">{{ $current->body_age ?? 'N/A' }}yrs</td>
|
<td class="small text-end fw-semibold text-primary">{{ $current->body_age ?? 'N/A' }}yrs</td>
|
||||||
<td class="small text-end text-muted">{{ $previous->body_age ?? 'N/A' }}yrs</td>
|
<td class="small text-end text-danger">{{ $previous->body_age ?? 'N/A' }}yrs</td>
|
||||||
<td class="text-center">{!! $current->body_age && $previous->body_age ? getChangeIcon($current->body_age, $previous->body_age) : '-' !!}</td>
|
<td class="text-center">{!! $current->body_age && $previous->body_age ? getChangeIcon($current->body_age, $previous->body_age) : '-' !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -718,6 +724,7 @@
|
|||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-muted small fw-semibold">Date</th>
|
<th class="text-muted small fw-semibold">Date</th>
|
||||||
|
<th class="text-muted small fw-semibold text-center"><i class="bi bi-rulers me-1"></i>Height (cm)</th>
|
||||||
<th class="text-muted small fw-semibold text-center"><i class="bi bi-speedometer2 me-1"></i>Weight (kg)</th>
|
<th class="text-muted small fw-semibold text-center"><i class="bi bi-speedometer2 me-1"></i>Weight (kg)</th>
|
||||||
<th class="text-muted small fw-semibold text-center"><i class="bi bi-activity me-1"></i>Body Fat %</th>
|
<th class="text-muted small fw-semibold text-center"><i class="bi bi-activity me-1"></i>Body Fat %</th>
|
||||||
<th class="text-muted small fw-semibold text-center"><i class="bi bi-calculator me-1"></i>BMI</th>
|
<th class="text-muted small fw-semibold text-center"><i class="bi bi-calculator me-1"></i>BMI</th>
|
||||||
@ -734,6 +741,7 @@
|
|||||||
@foreach($healthRecords as $record)
|
@foreach($healthRecords as $record)
|
||||||
<tr data-record-id="{{ $record->id }}" class="position-relative history-row">
|
<tr data-record-id="{{ $record->id }}" class="position-relative history-row">
|
||||||
<td class="small fw-semibold">{{ $record->recorded_at->format('M j, Y') }}</td>
|
<td class="small fw-semibold">{{ $record->recorded_at->format('M j, Y') }}</td>
|
||||||
|
<td class="small text-center">{{ $record->height ?? '-' }}</td>
|
||||||
<td class="small text-center">{{ $record->weight ?? '-' }}</td>
|
<td class="small text-center">{{ $record->weight ?? '-' }}</td>
|
||||||
<td class="small text-center">{{ $record->body_fat_percentage ?? '-' }}</td>
|
<td class="small text-center">{{ $record->body_fat_percentage ?? '-' }}</td>
|
||||||
<td class="small text-center">{{ $record->bmi ?? '-' }}</td>
|
<td class="small text-center">{{ $record->bmi ?? '-' }}</td>
|
||||||
@ -772,8 +780,125 @@
|
|||||||
<div class="tab-pane fade" id="goals" role="tabpanel">
|
<div class="tab-pane fade" id="goals" role="tabpanel">
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<h5 class="fw-bold mb-3"><i class="bi bi-bullseye me-2"></i>Goals & Progress</h5>
|
<!-- Section Title & Subtitle -->
|
||||||
<p class="text-muted">Goal tracking coming soon...</p>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h5 class="fw-bold mb-1"><i class="bi bi-bullseye me-2"></i>Goal Tracking</h5>
|
||||||
|
<p class="text-muted small mb-0">Set, track, and achieve your fitness objectives.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Cards -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<!-- Active Goals -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm border-0 text-center h-100 goal-filter-card" style="background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%); min-height: 120px; cursor: pointer;" data-filter="active">
|
||||||
|
<div class="card-body p-3 d-flex flex-column justify-content-center align-items-center h-100">
|
||||||
|
<i class="bi bi-bullseye text-white mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="text-white fw-bold mb-1">{{ $activeGoalsCount }}</h4>
|
||||||
|
<small class="text-white-50">Active Goals</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Completed Goals -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm border-0 text-center h-100 goal-filter-card" style="background: linear-gradient(135deg, #10b981 0%, #34d399 100%); min-height: 120px; cursor: pointer;" data-filter="completed">
|
||||||
|
<div class="card-body p-3 d-flex flex-column justify-content-center align-items-center h-100">
|
||||||
|
<i class="bi bi-check-circle-fill text-white mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="text-white fw-bold mb-1">{{ $completedGoalsCount }}</h4>
|
||||||
|
<small class="text-white-50">Completed Goals</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Success Rate -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm border-0 text-center h-100" style="background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%); min-height: 120px;">
|
||||||
|
<div class="card-body p-3 d-flex flex-column justify-content-center align-items-center h-100">
|
||||||
|
<i class="bi bi-graph-up-arrow text-white mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="text-white fw-bold mb-1">{{ $successRate }}%</h4>
|
||||||
|
<small class="text-white-50">Success Rate</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Goals List -->
|
||||||
|
@if($goals->count() > 0)
|
||||||
|
<div class="row g-4">
|
||||||
|
@foreach($goals as $goal)
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow-sm border-0 h-100 position-relative">
|
||||||
|
<!-- Edit Button (only for active goals and authorized users) -->
|
||||||
|
@if($goal->status == 'active' && ($relationship->relationship_type == 'self' || Auth::id() == $relationship->guardian_user_id))
|
||||||
|
<button class="btn btn-sm btn-outline-primary rounded-circle position-absolute top-0 end-0 mt-2 me-2 edit-goal-btn" style="width: 32px; height: 32px; padding: 0;" data-goal-id="{{ $goal->id }}" title="Edit Goal">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<!-- Title & Icon -->
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 48px; height: 48px; background-color: #8b5cf6;">
|
||||||
|
@if($goal->icon_type == 'dumbbell')
|
||||||
|
<i class="bi bi-dumbbell text-white"></i>
|
||||||
|
@elseif($goal->icon_type == 'clock')
|
||||||
|
<i class="bi bi-clock text-white"></i>
|
||||||
|
@else
|
||||||
|
<i class="bi bi-bullseye text-white"></i>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="fw-bold mb-1">{{ $goal->title }}</h6>
|
||||||
|
@if($goal->description)
|
||||||
|
<p class="text-muted small mb-0">{{ $goal->description }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Indicator -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<small class="text-muted">Progress: {{ number_format($goal->current_progress_value, 1) }} / {{ number_format($goal->target_value, 1) }} {{ $goal->unit }}</small>
|
||||||
|
<small class="fw-semibold">{{ number_format($goal->progress_percentage, 1) }}%</small>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 8px;">
|
||||||
|
<div class="progress-bar" role="progressbar" style="width: {{ $goal->progress_percentage }}%; background: linear-gradient(90deg, #8b5cf6 0%, #10b981 100%);" aria-valuenow="{{ $goal->progress_percentage }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dates & Status -->
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<small class="text-muted d-block">Started:</small>
|
||||||
|
<small class="fw-semibold">{{ $goal->start_date->format('M d, Y') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<small class="text-muted d-block">Target:</small>
|
||||||
|
<small class="fw-semibold">{{ $goal->target_date->format('M d, Y') }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Badges -->
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<span class="badge {{ $goal->status == 'active' ? 'bg-primary' : 'bg-success' }} small">
|
||||||
|
{{ ucfirst($goal->status) }}
|
||||||
|
</span>
|
||||||
|
<span class="badge {{ $goal->priority_level == 'high' ? 'bg-danger' : ($goal->priority_level == 'medium' ? 'bg-warning text-dark' : 'bg-secondary') }} small">
|
||||||
|
{{ ucfirst($goal->priority_level) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-bullseye text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<h5 class="text-muted mt-3 mb-2">No Goals Set Yet</h5>
|
||||||
|
<p class="text-muted mb-0">Start your fitness journey by setting your first goal!</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -792,8 +917,160 @@
|
|||||||
<div class="tab-pane fade" id="tournaments" role="tabpanel">
|
<div class="tab-pane fade" id="tournaments" role="tabpanel">
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<h5 class="fw-bold mb-3"><i class="bi bi-award me-2"></i>Tournament History</h5>
|
<!-- Section Title & Subtitle -->
|
||||||
<p class="text-muted">Tournament records coming soon...</p>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h5 class="fw-bold mb-1"><i class="bi bi-trophy-fill text-warning me-2"></i>Tournament & Event Participation</h5>
|
||||||
|
<p class="text-muted small mb-0">Proven champion with multiple championship wins and prestigious awards.</p>
|
||||||
|
</div>
|
||||||
|
<!-- Filter Section -->
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<label for="sportFilter" class="form-label me-2 mb-0 fw-semibold">Filter by Sport:</label>
|
||||||
|
<select class="form-select form-select-sm" id="sportFilter" style="width: 150px;">
|
||||||
|
<option value="all">All Sports</option>
|
||||||
|
@foreach($sports as $sport)
|
||||||
|
<option value="{{ $sport }}">{{ $sport }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Award Summary Cards -->
|
||||||
|
<div class="row g-3 mb-4" id="awardCards">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm border-0 text-center" style="background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<i class="bi bi-trophy-fill text-white mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="text-white fw-bold mb-1" id="specialCount">{{ $awardCounts['special'] }}</h4>
|
||||||
|
<small class="text-white-50">Special Award</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm border-0 text-center" style="background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<i class="bi bi-award-fill text-white mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="text-white fw-bold mb-1" id="firstCount">{{ $awardCounts['1st'] }}</h4>
|
||||||
|
<small class="text-white-50">1st Place</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm border-0 text-center" style="background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%);">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<i class="bi bi-award-fill text-white mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="text-white fw-bold mb-1" id="secondCount">{{ $awardCounts['2nd'] }}</h4>
|
||||||
|
<small class="text-white-50">2nd Place</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm border-0 text-center" style="background: linear-gradient(135deg, #CD7F32 0%, #A0522D 100%);">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<i class="bi bi-award-fill text-white mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="text-white fw-bold mb-1" id="thirdCount">{{ $awardCounts['3rd'] }}</h4>
|
||||||
|
<small class="text-white-50">3rd Place</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tournament History Table -->
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-list-ul me-2"></i>Tournament & Championships History</h6>
|
||||||
|
|
||||||
|
@if($tournamentEvents->count() > 0)
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle" id="tournamentsTable">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th class="text-muted small fw-semibold">Tournament Details</th>
|
||||||
|
<th class="text-muted small fw-semibold">Performance & Result</th>
|
||||||
|
<th class="text-muted small fw-semibold">Notes & Media</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($tournamentEvents as $event)
|
||||||
|
<tr data-sport="{{ $event->sport }}">
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold">{{ $event->title }}</div>
|
||||||
|
<div class="d-flex gap-2 mt-1 flex-wrap">
|
||||||
|
<span class="badge bg-{{ $event->type == 'championship' ? 'primary' : 'secondary' }} small">{{ ucfirst($event->type) }}</span>
|
||||||
|
<span class="badge bg-info small">{{ $event->sport }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mt-1">
|
||||||
|
<i class="bi bi-calendar-event me-1"></i>{{ $event->date->format('M j, Y') }}
|
||||||
|
@if($event->time)
|
||||||
|
<i class="bi bi-clock me-1 ms-2"></i>{{ $event->time->format('H:i') }}
|
||||||
|
@endif
|
||||||
|
@if($event->location)
|
||||||
|
<i class="bi bi-geo-alt me-1 ms-2"></i>{{ $event->location }}
|
||||||
|
@endif
|
||||||
|
@if($event->participants_count)
|
||||||
|
<i class="bi bi-people me-1 ms-2"></i>{{ $event->participants_count }} participants
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($event->performanceResults->count() > 0)
|
||||||
|
@foreach($event->performanceResults as $result)
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
@if($result->medal_type == '1st')
|
||||||
|
<i class="bi bi-award-fill text-warning"></i>
|
||||||
|
<span class="badge bg-warning text-dark small">1st Place</span>
|
||||||
|
@elseif($result->medal_type == '2nd')
|
||||||
|
<i class="bi bi-award-fill text-secondary"></i>
|
||||||
|
<span class="badge bg-secondary small">2nd Place</span>
|
||||||
|
@elseif($result->medal_type == '3rd')
|
||||||
|
<i class="bi bi-award-fill" style="color: #CD7F32;"></i>
|
||||||
|
<span class="badge" style="background-color: #CD7F32; color: white;" small>3rd Place</span>
|
||||||
|
@elseif($result->medal_type == 'special')
|
||||||
|
<i class="bi bi-trophy-fill text-warning"></i>
|
||||||
|
<span class="badge bg-warning text-dark small">Special Award</span>
|
||||||
|
@endif
|
||||||
|
@if($result->points)
|
||||||
|
<small class="text-muted">{{ $result->points }} pts</small>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if($result->description)
|
||||||
|
<small class="text-muted">{{ $result->description }}</small>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
<span class="text-muted small">No results recorded</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($event->notesMedia->count() > 0)
|
||||||
|
@foreach($event->notesMedia as $note)
|
||||||
|
@if($note->note_text)
|
||||||
|
<p class="mb-1 small">{{ $note->note_text }}</p>
|
||||||
|
@endif
|
||||||
|
@if($note->media_link)
|
||||||
|
<a href="{{ $note->media_link }}" target="_blank" class="btn btn-sm btn-outline-primary small">
|
||||||
|
<i class="bi bi-image me-1"></i>View Media
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
<span class="text-muted small">No notes available</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-trophy text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">No tournament records found</p>
|
||||||
|
<small class="text-muted">Tournament participation will appear here once records are added</small>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -810,6 +1087,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Goal Edit Modal -->
|
||||||
|
<div class="modal fade" id="goalEditModal" tabindex="-1" aria-labelledby="goalEditModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="goalEditModalLabel">Edit Goal Progress</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form id="goalEditForm" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="rounded-circle d-flex align-items-center justify-content-center me-3" id="goalIconDisplay" style="width: 48px; height: 48px; background-color: #8b5cf6;">
|
||||||
|
<i class="bi bi-bullseye text-white"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="fw-bold mb-1" id="goalTitleDisplay">Goal Title</h6>
|
||||||
|
<p class="text-muted small mb-0" id="goalDescriptionDisplay">Goal description</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="current_progress_value" class="form-label">Current Progress <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" step="0.1" class="form-control" id="current_progress_value" name="current_progress_value" required>
|
||||||
|
<span class="input-group-text" id="goalUnitDisplay">lbs</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Target: <span id="goalTargetDisplay">170.0 lbs</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="goal_status" class="form-label">Status</label>
|
||||||
|
<select class="form-select" id="goal_status" name="status">
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="progress" style="height: 8px;">
|
||||||
|
<div class="progress-bar" role="progressbar" id="progressPreview" style="width: 0%; background: linear-gradient(90deg, #8b5cf6 0%, #10b981 100%);"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted mt-1 d-block" id="progressTextPreview">Progress: 0.0 / 170.0 lbs (0.0%)</small>
|
||||||
|
</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">Update Goal</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Health Update Modal -->
|
<!-- Health Update Modal -->
|
||||||
<div class="modal fade" id="healthUpdateModal" tabindex="-1" aria-labelledby="healthUpdateModalLabel" aria-hidden="true">
|
<div class="modal fade" id="healthUpdateModal" tabindex="-1" aria-labelledby="healthUpdateModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
@ -826,6 +1159,10 @@
|
|||||||
<label for="recorded_at" class="form-label">Date <span class="text-danger">*</span></label>
|
<label for="recorded_at" class="form-label">Date <span class="text-danger">*</span></label>
|
||||||
<input type="date" class="form-control" id="recorded_at" name="recorded_at" value="{{ \Carbon\Carbon::now()->format('Y-m-d') }}" required>
|
<input type="date" class="form-control" id="recorded_at" name="recorded_at" value="{{ \Carbon\Carbon::now()->format('Y-m-d') }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="height" class="form-label">Height (cm)</label>
|
||||||
|
<input type="number" step="0.1" class="form-control" id="height" name="height">
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="weight" class="form-label">Weight (kg)</label>
|
<label for="weight" class="form-label">Weight (kg)</label>
|
||||||
<input type="number" step="0.1" class="form-control" id="weight" name="weight">
|
<input type="number" step="0.1" class="form-control" id="weight" name="weight">
|
||||||
@ -942,20 +1279,17 @@
|
|||||||
|
|
||||||
// Radar chart variables
|
// Radar chart variables
|
||||||
let radarChart = null;
|
let radarChart = null;
|
||||||
const metricLabels = ['Weight', 'Body Fat', 'BMI', 'Body Water', 'Muscle Mass', 'Bone Mass', 'Visceral Fat', 'BMR', 'Protein', 'Body Age'];
|
const metricLabels = ['Height', 'Weight', 'Body Fat', 'BMI', 'Body Water', 'Muscle Mass', 'Bone Mass', 'Visceral Fat', 'BMR', 'Protein', 'Body Age'];
|
||||||
const metricKeys = ['weight', 'body_fat_percentage', 'bmi', 'body_water_percentage', 'muscle_mass', 'bone_mass', 'visceral_fat', 'bmr', 'protein_percentage', 'body_age'];
|
const metricKeys = ['height', 'weight', 'body_fat_percentage', 'bmi', 'body_water_percentage', 'muscle_mass', 'bone_mass', 'visceral_fat', 'bmr', 'protein_percentage', 'body_age'];
|
||||||
|
|
||||||
// Function to create/update radar chart
|
// Function to create/update radar chart
|
||||||
function updateRadarChart(currentRecord, previousRecord) {
|
function updateRadarChart(currentRecord, previousRecord) {
|
||||||
console.log('updateRadarChart called', currentRecord, previousRecord);
|
console.log('updateRadarChart called', currentRecord, previousRecord);
|
||||||
const ctx = document.getElementById('radarChart').getContext('2d');
|
const ctx = document.getElementById('radarChart').getContext('2d');
|
||||||
console.log('ctx', ctx);
|
|
||||||
|
|
||||||
|
// Extract data for current and previous records
|
||||||
const currentData = metricKeys.map(key => currentRecord ? (currentRecord[key] || 0) : 0);
|
const currentData = metricKeys.map(key => currentRecord ? (currentRecord[key] || 0) : 0);
|
||||||
const previousData = metricKeys.map(key => previousRecord ? (previousRecord[key] || 0) : 0);
|
const previousData = metricKeys.map(key => previousRecord ? (previousRecord[key] || 0) : 0);
|
||||||
console.log('currentData', currentData);
|
|
||||||
console.log('previousData', previousData);
|
|
||||||
console.log('window.Chart', window.Chart);
|
|
||||||
|
|
||||||
if (radarChart) {
|
if (radarChart) {
|
||||||
radarChart.destroy();
|
radarChart.destroy();
|
||||||
@ -967,30 +1301,36 @@
|
|||||||
labels: metricLabels,
|
labels: metricLabels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'From Record',
|
label: 'Current Reading',
|
||||||
data: previousData,
|
data: currentData,
|
||||||
borderColor: 'rgba(54, 162, 235, 1)',
|
fill: true,
|
||||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||||
pointBackgroundColor: 'rgba(54, 162, 235, 1)',
|
borderColor: 'rgb(54, 162, 235)',
|
||||||
|
pointBackgroundColor: 'rgb(54, 162, 235)',
|
||||||
pointBorderColor: '#fff',
|
pointBorderColor: '#fff',
|
||||||
pointHoverBackgroundColor: '#fff',
|
pointHoverBackgroundColor: '#fff',
|
||||||
pointHoverBorderColor: 'rgba(54, 162, 235, 1)',
|
pointHoverBorderColor: 'rgb(54, 162, 235)'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'To Record',
|
label: 'Previous Reading',
|
||||||
data: currentData,
|
data: previousData,
|
||||||
borderColor: 'rgba(255, 99, 132, 1)',
|
fill: false,
|
||||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
pointBackgroundColor: 'rgba(255, 99, 132, 1)',
|
pointBackgroundColor: 'rgb(255, 99, 132)',
|
||||||
pointBorderColor: '#fff',
|
pointBorderColor: '#fff',
|
||||||
pointHoverBackgroundColor: '#fff',
|
pointHoverBackgroundColor: '#fff',
|
||||||
pointHoverBorderColor: 'rgba(255, 99, 132, 1)',
|
pointHoverBorderColor: 'rgb(255, 99, 132)'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
elements: {
|
||||||
|
line: {
|
||||||
|
borderWidth: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: 'top',
|
position: 'top',
|
||||||
@ -1020,12 +1360,32 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to calculate BMI
|
||||||
|
function calculateBMI() {
|
||||||
|
const height = parseFloat(document.getElementById('height').value);
|
||||||
|
const weight = parseFloat(document.getElementById('weight').value);
|
||||||
|
const bmiField = document.getElementById('bmi');
|
||||||
|
|
||||||
|
if (height > 0 && weight > 0) {
|
||||||
|
const heightInMeters = height / 100;
|
||||||
|
const bmi = weight / (heightInMeters * heightInMeters);
|
||||||
|
bmiField.value = bmi.toFixed(1);
|
||||||
|
} else {
|
||||||
|
bmiField.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners for BMI calculation
|
||||||
|
document.getElementById('height').addEventListener('input', calculateBMI);
|
||||||
|
document.getElementById('weight').addEventListener('input', calculateBMI);
|
||||||
|
|
||||||
// Function to reset modal for adding new record
|
// Function to reset modal for adding new record
|
||||||
function resetHealthModal() {
|
function resetHealthModal() {
|
||||||
document.getElementById('healthUpdateModalLabel').textContent = 'Add Health Update';
|
document.getElementById('healthUpdateModalLabel').textContent = 'Add Health Update';
|
||||||
document.getElementById('healthUpdateForm').action = '{{ route("family.store-health", $relationship->dependent->id) }}';
|
document.getElementById('healthUpdateForm').action = '{{ route("family.store-health", $relationship->dependent->id) }}';
|
||||||
document.getElementById('healthUpdateForm').method = 'POST';
|
document.getElementById('healthUpdateForm').method = 'POST';
|
||||||
document.getElementById('recorded_at').value = '{{ \Carbon\Carbon::now()->format("Y-m-d") }}';
|
document.getElementById('recorded_at').value = '{{ \Carbon\Carbon::now()->format("Y-m-d") }}';
|
||||||
|
document.getElementById('height').value = '';
|
||||||
document.getElementById('weight').value = '';
|
document.getElementById('weight').value = '';
|
||||||
document.getElementById('body_fat_percentage').value = '';
|
document.getElementById('body_fat_percentage').value = '';
|
||||||
document.getElementById('bmi').value = '';
|
document.getElementById('bmi').value = '';
|
||||||
@ -1059,6 +1419,7 @@
|
|||||||
methodInput.value = 'PUT';
|
methodInput.value = 'PUT';
|
||||||
|
|
||||||
document.getElementById('recorded_at').value = record.recorded_at.split('T')[0];
|
document.getElementById('recorded_at').value = record.recorded_at.split('T')[0];
|
||||||
|
document.getElementById('height').value = record.height || '';
|
||||||
document.getElementById('weight').value = record.weight || '';
|
document.getElementById('weight').value = record.weight || '';
|
||||||
document.getElementById('body_fat_percentage').value = record.body_fat_percentage || '';
|
document.getElementById('body_fat_percentage').value = record.body_fat_percentage || '';
|
||||||
document.getElementById('bmi').value = record.bmi || '';
|
document.getElementById('bmi').value = record.bmi || '';
|
||||||
@ -1099,6 +1460,7 @@
|
|||||||
document.getElementById('timeDifference').innerHTML = `<strong>Time between records:</strong> ${timeDiff}`;
|
document.getElementById('timeDifference').innerHTML = `<strong>Time between records:</strong> ${timeDiff}`;
|
||||||
|
|
||||||
// Update the table rows
|
// Update the table rows
|
||||||
|
updateTableRow('height', currentRecord.height, previousRecord.height);
|
||||||
updateTableRow('weight', currentRecord.weight, previousRecord.weight);
|
updateTableRow('weight', currentRecord.weight, previousRecord.weight);
|
||||||
updateTableRow('body_fat', currentRecord.body_fat_percentage, previousRecord.body_fat_percentage);
|
updateTableRow('body_fat', currentRecord.body_fat_percentage, previousRecord.body_fat_percentage);
|
||||||
updateTableRow('bmi', currentRecord.bmi, previousRecord.bmi);
|
updateTableRow('bmi', currentRecord.bmi, previousRecord.bmi);
|
||||||
@ -1138,7 +1500,9 @@
|
|||||||
const cells = row.querySelectorAll('td');
|
const cells = row.querySelectorAll('td');
|
||||||
if (cells.length >= 4) {
|
if (cells.length >= 4) {
|
||||||
// Update current value
|
// Update current value
|
||||||
if (metric === 'weight' || metric === 'muscle_mass' || metric === 'bone_mass') {
|
if (metric === 'height') {
|
||||||
|
cells[1].textContent = currentValue ? `${currentValue}cm` : 'N/A';
|
||||||
|
} else if (metric === 'weight' || metric === 'muscle_mass' || metric === 'bone_mass') {
|
||||||
cells[1].textContent = currentValue ? `${currentValue}kg` : 'N/A';
|
cells[1].textContent = currentValue ? `${currentValue}kg` : 'N/A';
|
||||||
} else if (metric === 'body_fat' || metric === 'body_water' || metric === 'protein') {
|
} else if (metric === 'body_fat' || metric === 'body_water' || metric === 'protein') {
|
||||||
cells[1].textContent = currentValue ? `${currentValue}%` : 'N/A';
|
cells[1].textContent = currentValue ? `${currentValue}%` : 'N/A';
|
||||||
@ -1151,7 +1515,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update previous value
|
// Update previous value
|
||||||
if (metric === 'weight' || metric === 'muscle_mass' || metric === 'bone_mass') {
|
if (metric === 'height') {
|
||||||
|
cells[2].textContent = previousValue ? `${previousValue}cm` : 'N/A';
|
||||||
|
} else if (metric === 'weight' || metric === 'muscle_mass' || metric === 'bone_mass') {
|
||||||
cells[2].textContent = previousValue ? `${previousValue}kg` : 'N/A';
|
cells[2].textContent = previousValue ? `${previousValue}kg` : 'N/A';
|
||||||
} else if (metric === 'body_fat' || metric === 'body_water' || metric === 'protein') {
|
} else if (metric === 'body_fat' || metric === 'body_water' || metric === 'protein') {
|
||||||
cells[2].textContent = previousValue ? `${previousValue}%` : 'N/A';
|
cells[2].textContent = previousValue ? `${previousValue}%` : 'N/A';
|
||||||
@ -1211,6 +1577,214 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
// Tournament filtering functionality
|
||||||
|
const sportFilter = document.getElementById('sportFilter');
|
||||||
|
const tournamentsTable = document.getElementById('tournamentsTable');
|
||||||
|
const awardCards = document.getElementById('awardCards');
|
||||||
|
|
||||||
|
if (sportFilter && tournamentsTable) {
|
||||||
|
sportFilter.addEventListener('change', function() {
|
||||||
|
const selectedSport = this.value;
|
||||||
|
const rows = tournamentsTable.querySelectorAll('tbody tr');
|
||||||
|
|
||||||
|
let visibleRows = 0;
|
||||||
|
let specialCount = 0, firstCount = 0, secondCount = 0, thirdCount = 0;
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const sport = row.getAttribute('data-sport');
|
||||||
|
if (selectedSport === 'all' || sport === selectedSport) {
|
||||||
|
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 => {
|
||||||
|
if (badge.textContent.includes('Special Award')) specialCount++;
|
||||||
|
else if (badge.textContent.includes('1st Place')) firstCount++;
|
||||||
|
else if (badge.textContent.includes('2nd Place')) secondCount++;
|
||||||
|
else if (badge.textContent.includes('3rd Place')) thirdCount++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update award counts
|
||||||
|
document.getElementById('specialCount').textContent = specialCount;
|
||||||
|
document.getElementById('firstCount').textContent = firstCount;
|
||||||
|
document.getElementById('secondCount').textContent = secondCount;
|
||||||
|
document.getElementById('thirdCount').textContent = thirdCount;
|
||||||
|
|
||||||
|
// Show/hide award cards based on visible rows
|
||||||
|
if (visibleRows === 0) {
|
||||||
|
awardCards.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
awardCards.style.display = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goals filtering functionality
|
||||||
|
const goalFilterCards = document.querySelectorAll('.goal-filter-card');
|
||||||
|
const goalsContainer = document.querySelector('.row.g-4'); // Container with goal cards
|
||||||
|
const goalsTitle = document.querySelector('h5.fw-bold'); // The "Goal Tracking" title
|
||||||
|
|
||||||
|
if (goalFilterCards.length > 0 && goalsContainer) {
|
||||||
|
// Add click event to filter cards
|
||||||
|
goalFilterCards.forEach(card => {
|
||||||
|
card.addEventListener('click', function() {
|
||||||
|
const filterType = this.getAttribute('data-filter');
|
||||||
|
filterGoals(filterType);
|
||||||
|
|
||||||
|
// Update card styles to show active filter
|
||||||
|
goalFilterCards.forEach(c => c.classList.remove('border', 'border-primary', 'shadow-lg'));
|
||||||
|
this.classList.add('border', 'border-primary', 'shadow-lg');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add click event to title to show all goals
|
||||||
|
if (goalsTitle) {
|
||||||
|
goalsTitle.style.cursor = 'pointer';
|
||||||
|
goalsTitle.addEventListener('click', function() {
|
||||||
|
filterGoals('all');
|
||||||
|
// Remove active styles from filter cards
|
||||||
|
goalFilterCards.forEach(c => c.classList.remove('border', 'border-primary', 'shadow-lg'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterGoals(filterType) {
|
||||||
|
const goalCards = goalsContainer.querySelectorAll('.col-lg-6');
|
||||||
|
|
||||||
|
goalCards.forEach(card => {
|
||||||
|
const statusBadge = card.querySelector('.badge');
|
||||||
|
if (!statusBadge) return;
|
||||||
|
|
||||||
|
const statusText = statusBadge.textContent.toLowerCase();
|
||||||
|
|
||||||
|
if (filterType === 'all') {
|
||||||
|
card.style.display = '';
|
||||||
|
} else if (filterType === 'active' && statusText === 'active') {
|
||||||
|
card.style.display = '';
|
||||||
|
} else if (filterType === 'completed' && statusText === 'completed') {
|
||||||
|
card.style.display = '';
|
||||||
|
} else {
|
||||||
|
card.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update title to show current filter
|
||||||
|
const titleElement = document.querySelector('h5.fw-bold');
|
||||||
|
if (titleElement) {
|
||||||
|
const baseTitle = 'Goal Tracking';
|
||||||
|
if (filterType === 'all') {
|
||||||
|
titleElement.innerHTML = `<i class="bi bi-bullseye me-2"></i>${baseTitle}`;
|
||||||
|
} else {
|
||||||
|
const filterLabel = filterType === 'active' ? 'Active' : 'Completed';
|
||||||
|
titleElement.innerHTML = `<i class="bi bi-bullseye me-2"></i>${baseTitle} - ${filterLabel} Goals`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goal editing functionality
|
||||||
|
const editGoalButtons = document.querySelectorAll('.edit-goal-btn');
|
||||||
|
const goalEditModal = new bootstrap.Modal(document.getElementById('goalEditModal'));
|
||||||
|
const goalEditForm = document.getElementById('goalEditForm');
|
||||||
|
let currentGoalId = null;
|
||||||
|
|
||||||
|
// Store goals data for editing
|
||||||
|
const goalsData = @json($goals);
|
||||||
|
|
||||||
|
if (editGoalButtons.length > 0) {
|
||||||
|
editGoalButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const goalId = this.getAttribute('data-goal-id');
|
||||||
|
populateGoalEditModal(goalId);
|
||||||
|
goalEditModal.show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateGoalEditModal(goalId) {
|
||||||
|
const goal = goalsData.find(g => g.id == goalId);
|
||||||
|
if (!goal) return;
|
||||||
|
|
||||||
|
currentGoalId = goalId;
|
||||||
|
|
||||||
|
// Update form action
|
||||||
|
goalEditForm.action = `/family/goal/${goalId}`;
|
||||||
|
|
||||||
|
// Populate modal fields
|
||||||
|
document.getElementById('goalTitleDisplay').textContent = goal.title;
|
||||||
|
document.getElementById('goalDescriptionDisplay').textContent = goal.description || 'No description';
|
||||||
|
document.getElementById('current_progress_value').value = goal.current_progress_value;
|
||||||
|
document.getElementById('goal_status').value = goal.status;
|
||||||
|
document.getElementById('goalUnitDisplay').textContent = goal.unit;
|
||||||
|
document.getElementById('goalTargetDisplay').textContent = `${goal.target_value} ${goal.unit}`;
|
||||||
|
|
||||||
|
// Update icon
|
||||||
|
const iconElement = document.getElementById('goalIconDisplay').querySelector('i');
|
||||||
|
if (goal.icon_type === 'dumbbell') {
|
||||||
|
iconElement.className = 'bi bi-dumbbell text-white';
|
||||||
|
} else if (goal.icon_type === 'clock') {
|
||||||
|
iconElement.className = 'bi bi-clock text-white';
|
||||||
|
} else {
|
||||||
|
iconElement.className = 'bi bi-bullseye text-white';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress preview
|
||||||
|
updateProgressPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgressPreview() {
|
||||||
|
const currentValue = parseFloat(document.getElementById('current_progress_value').value) || 0;
|
||||||
|
const goal = goalsData.find(g => g.id == currentGoalId);
|
||||||
|
if (!goal) return;
|
||||||
|
|
||||||
|
const targetValue = goal.target_value;
|
||||||
|
const percentage = targetValue > 0 ? Math.min(100, (currentValue / targetValue) * 100) : 0;
|
||||||
|
|
||||||
|
document.getElementById('progressPreview').style.width = `${percentage}%`;
|
||||||
|
document.getElementById('progressTextPreview').textContent = `Progress: ${currentValue.toFixed(1)} / ${targetValue.toFixed(1)} ${goal.unit} (${percentage.toFixed(1)}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress preview on input change
|
||||||
|
document.getElementById('current_progress_value').addEventListener('input', updateProgressPreview);
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
goalEditForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
formData.append('_method', 'PUT');
|
||||||
|
|
||||||
|
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) {
|
||||||
|
goalEditModal.hide();
|
||||||
|
// Reload the page to show updated data
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error updating goal: ' + (data.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error updating goal. Please try again.');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -443,6 +443,9 @@
|
|||||||
<!-- Select2 JS -->
|
<!-- Select2 JS -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Vite Compiled Assets -->
|
||||||
|
@vite(['resources/js/app.js'])
|
||||||
|
|
||||||
@stack('scripts')
|
@stack('scripts')
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -94,6 +94,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
Route::put('/family/{id}', [FamilyController::class, 'update'])->name('family.update');
|
Route::put('/family/{id}', [FamilyController::class, 'update'])->name('family.update');
|
||||||
Route::post('/family/{id}/health', [FamilyController::class, 'storeHealth'])->name('family.store-health');
|
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/{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}/upload-picture', [FamilyController::class, 'uploadFamilyMemberPicture'])->name('family.upload-picture');
|
Route::post('/family/{id}/upload-picture', [FamilyController::class, 'uploadFamilyMemberPicture'])->name('family.upload-picture');
|
||||||
Route::delete('/family/{id}', [FamilyController::class, 'destroy'])->name('family.destroy');
|
Route::delete('/family/{id}', [FamilyController::class, 'destroy'])->name('family.destroy');
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user