added attendance and fixed body composition

This commit is contained in:
Ghassan Yusuf 2026-01-24 13:11:20 +03:00
parent 87051e179d
commit 125e1c34af
8 changed files with 293 additions and 4 deletions

View File

@ -8,6 +8,7 @@ use App\Models\HealthRecord;
use App\Models\Invoice;
use App\Models\TournamentEvent;
use App\Models\Goal;
use App\Models\Attendance;
use App\Services\FamilyService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -80,6 +81,13 @@ class FamilyController extends Controller
$completedGoalsCount = $goals->where('status', 'completed')->count();
$successRate = $goals->count() > 0 ? round(($completedGoalsCount / $goals->count()) * 100) : 0;
// Fetch attendance data
$attendanceRecords = $user->attendanceRecords()->orderBy('session_datetime', 'desc')->get();
$sessionsCompleted = $attendanceRecords->where('status', 'completed')->count();
$noShows = $attendanceRecords->where('status', 'no_show')->count();
$totalSessions = $attendanceRecords->count();
$attendanceRate = $totalSessions > 0 ? round(($sessionsCompleted / $totalSessions) * 100, 1) : 0;
// Pass user directly and a flag to indicate it's the current user's profile
return view('family.show', [
'relationship' => (object)[
@ -99,6 +107,10 @@ class FamilyController extends Controller
'activeGoalsCount' => $activeGoalsCount,
'completedGoalsCount' => $completedGoalsCount,
'successRate' => $successRate,
'attendanceRecords' => $attendanceRecords,
'sessionsCompleted' => $sessionsCompleted,
'noShows' => $noShows,
'attendanceRate' => $attendanceRate,
]);
}
@ -289,7 +301,14 @@ class FamilyController extends Controller
$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'));
// Fetch attendance data for the dependent
$attendanceRecords = $relationship->dependent->attendanceRecords()->orderBy('session_datetime', 'desc')->get();
$sessionsCompleted = $attendanceRecords->where('status', 'completed')->count();
$noShows = $attendanceRecords->where('status', 'no_show')->count();
$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'));
}
/**

29
app/Models/Attendance.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Attendance extends Model
{
protected $table = 'members_attendance';
protected $fillable = [
'member_id',
'session_type',
'trainer_name',
'session_datetime',
'status',
'notes',
];
protected $casts = [
'session_datetime' => 'datetime',
];
public function member(): BelongsTo
{
return $this->belongsTo(User::class, 'member_id');
}
}

View File

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

View File

@ -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('attendance_records', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->string('member_type')->default('Basic'); // Premium, Basic, Student, Family
$table->datetime('check_in_time');
$table->datetime('check_out_time')->nullable();
$table->string('duration')->nullable(); // e.g., "1h 45m"
$table->string('activity'); // e.g., "Gym Floor", "Yoga Class"
$table->enum('status', ['active', 'completed'])->default('active');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('attendance_records');
}
};

View File

@ -0,0 +1,33 @@
<?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('members_attendance', function (Blueprint $table) {
$table->id();
$table->foreignId('member_id')->constrained('users')->onDelete('cascade');
$table->string('session_type'); // e.g. Personal Training, Group Fitness, Yoga, Pilates
$table->string('trainer_name');
$table->datetime('session_datetime');
$table->enum('status', ['completed', 'no_show']);
$table->text('notes')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('members_attendance');
}
};

View File

@ -0,0 +1,79 @@
<?php
namespace Database\Seeders;
use App\Models\Attendance;
use App\Models\User;
use App\Models\Tenant;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class AttendanceSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$user = User::where('email', 'test@example.com')->first();
if (!$user) {
return; // Skip if test data not found
}
$attendanceRecords = [
[
'member_id' => $user->id,
'session_type' => 'Personal Training',
'trainer_name' => 'John Smith',
'session_datetime' => now()->subDays(1)->setTime(10, 0),
'status' => 'completed',
'notes' => 'Great session, focused on upper body strength',
],
[
'member_id' => $user->id,
'session_type' => 'Group Fitness',
'trainer_name' => 'Sarah Johnson',
'session_datetime' => now()->subDays(2)->setTime(18, 30),
'status' => 'completed',
'notes' => 'High-intensity cardio workout',
],
[
'member_id' => $user->id,
'session_type' => 'Yoga',
'trainer_name' => 'Mike Wilson',
'session_datetime' => now()->subDays(3)->setTime(9, 0),
'status' => 'no_show',
'notes' => 'Member did not attend scheduled session',
],
[
'member_id' => $user->id,
'session_type' => 'Pilates',
'trainer_name' => 'Emily Davis',
'session_datetime' => now()->subDays(4)->setTime(17, 0),
'status' => 'completed',
'notes' => 'Core strengthening and flexibility',
],
[
'member_id' => $user->id,
'session_type' => 'Personal Training',
'trainer_name' => 'John Smith',
'session_datetime' => now()->subDays(5)->setTime(14, 0),
'status' => 'completed',
'notes' => 'Lower body strength training',
],
[
'member_id' => $user->id,
'session_type' => 'Group Fitness',
'trainer_name' => 'Sarah Johnson',
'session_datetime' => now()->subDays(6)->setTime(19, 0),
'status' => 'no_show',
'notes' => 'Late cancellation',
],
];
foreach ($attendanceRecords as $record) {
Attendance::create($record);
}
}
}

View File

@ -65,5 +65,8 @@ class DatabaseSeeder extends Seeder
'status' => 'pending',
'due_date' => now()->addDays(15),
]);
// Seed attendance data
$this->call(AttendanceSeeder::class);
}
}

View File

@ -431,8 +431,91 @@
<div class="tab-pane fade" id="attendance" 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-calendar-check me-2"></i>Attendance Records</h5>
<p class="text-muted">Attendance tracking coming soon...</p>
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h1 class="h3 fw-bold">Member Attendance</h1>
<p class="text-muted">Track your gym session attendance and performance</p>
</div>
</div>
<!-- Summary Cards -->
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card shadow-sm bg-light">
<div class="card-body text-center">
<div class="display-4 fw-bold text-success mb-2">{{ $sessionsCompleted }}</div>
<h6 class="card-title text-muted">Sessions Completed</h6>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm bg-light">
<div class="card-body text-center">
<div class="display-4 fw-bold text-danger mb-2">{{ $noShows }}</div>
<h6 class="card-title text-muted">No Shows</h6>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm bg-light">
<div class="card-body text-center">
<div class="display-4 fw-bold text-primary mb-2">{{ $attendanceRate }}%</div>
<h6 class="card-title text-muted">Attendance Rate</h6>
</div>
</div>
</div>
</div>
<!-- Attendance Table -->
<div class="card shadow-sm">
<div class="card-header bg-light">
<h6 class="card-title mb-0">Session History</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0 fw-semibold">Date & Time</th>
<th class="border-0 fw-semibold">Session Type</th>
<th class="border-0 fw-semibold">Trainer Name</th>
<th class="border-0 fw-semibold">Status</th>
<th class="border-0 fw-semibold">Notes</th>
</tr>
</thead>
<tbody>
@forelse($attendanceRecords as $record)
<tr>
<td class="align-middle">
<div class="fw-semibold">{{ $record->session_datetime->format('M j, Y') }}</div>
<small class="text-muted">{{ $record->session_datetime->format('g:i A') }}</small>
</td>
<td class="align-middle">{{ $record->session_type }}</td>
<td class="align-middle">{{ $record->trainer_name }}</td>
<td class="align-middle">
@if($record->status === 'completed')
<span class="badge bg-success">Completed</span>
@else
<span class="badge bg-danger">No Show</span>
@endif
</td>
<td class="align-middle">
<small class="text-muted">{{ $record->notes ?: '-' }}</small>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center py-4">
<i class="bi bi-calendar-check text-muted" style="font-size: 2rem;"></i>
<p class="text-muted mt-2 mb-0">No attendance records found</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
@ -1352,7 +1435,7 @@
r: {
beginAtZero: true,
ticks: {
stepSize: 10
display: false
}
}
}