From 6be7f4442a76398736a617bc9233983a5e0c87da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Tue, 16 Jun 2026 15:27:28 +0300 Subject: [PATCH] feat: add Gantt chart widget to visualize accepted intern schedules in the admin panel --- .../InternApplicationResource.php | 7 + .../Pages/ListInternApplications.php | 7 + .../Widgets/InternGanttChartWidget.php | 153 ++++++++++++++++++ .../intern-gantt-chart-widget.blade.php | 134 +++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 app/Filament/Admin/Resources/InternApplications/Widgets/InternGanttChartWidget.php create mode 100644 resources/views/filament/admin/resources/intern-applications/widgets/intern-gantt-chart-widget.blade.php diff --git a/app/Filament/Admin/Resources/InternApplications/InternApplicationResource.php b/app/Filament/Admin/Resources/InternApplications/InternApplicationResource.php index debe477..6df8774 100644 --- a/app/Filament/Admin/Resources/InternApplications/InternApplicationResource.php +++ b/app/Filament/Admin/Resources/InternApplications/InternApplicationResource.php @@ -362,4 +362,11 @@ class InternApplicationResource extends Resource 'edit' => Pages\EditInternApplication::route('/{record}/edit'), ]; } + + public static function getWidgets(): array + { + return [ + Widgets\InternGanttChartWidget::class, + ]; + } } diff --git a/app/Filament/Admin/Resources/InternApplications/Pages/ListInternApplications.php b/app/Filament/Admin/Resources/InternApplications/Pages/ListInternApplications.php index 1565e41..35d8a06 100644 --- a/app/Filament/Admin/Resources/InternApplications/Pages/ListInternApplications.php +++ b/app/Filament/Admin/Resources/InternApplications/Pages/ListInternApplications.php @@ -8,4 +8,11 @@ use Filament\Resources\Pages\ListRecords; class ListInternApplications extends ListRecords { protected static string $resource = InternApplicationResource::class; + + protected function getHeaderWidgets(): array + { + return [ + \App\Filament\Admin\Resources\InternApplications\Widgets\InternGanttChartWidget::class, + ]; + } } diff --git a/app/Filament/Admin/Resources/InternApplications/Widgets/InternGanttChartWidget.php b/app/Filament/Admin/Resources/InternApplications/Widgets/InternGanttChartWidget.php new file mode 100644 index 0000000..8ad4b5e --- /dev/null +++ b/app/Filament/Admin/Resources/InternApplications/Widgets/InternGanttChartWidget.php @@ -0,0 +1,153 @@ +where('type', 'internship') + ->where('status', 'accepted') + ->whereNotNull('internship_start_date') + ->whereNotNull('internship_end_date') + ->orderBy('internship_start_date', 'asc') + ->get(); + + if ($interns->isEmpty()) { + return [ + 'interns' => [], + 'months' => [], + 'totalDays' => 0, + 'minDate' => null, + 'todayPercent' => null, + ]; + } + + $minDate = Carbon::parse($interns->min('internship_start_date'))->startOfDay(); + $maxDate = Carbon::parse($interns->max('internship_end_date'))->endOfDay(); + + // Pad the range slightly for better visual presentation + $minDate = $minDate->copy()->subDays(5); + $maxDate = $maxDate->copy()->addDays(5); + + $totalDays = $minDate->diffInDays($maxDate); + if ($totalDays <= 0) { + $totalDays = 1; + } + + // Generate months in range + $months = []; + $tempDate = $minDate->copy()->startOfMonth(); + $endLimit = $maxDate->copy()->endOfMonth(); + + while ($tempDate->lte($endLimit)) { + $monthStart = $tempDate->copy()->startOfMonth(); + $monthEnd = $tempDate->copy()->endOfMonth(); + + $overlapStart = $monthStart->gt($minDate) ? $monthStart : $minDate; + $overlapEnd = $monthEnd->lt($maxDate) ? $monthEnd : $maxDate; + + if ($overlapStart->lte($overlapEnd)) { + $daysInOverlap = $overlapStart->diffInDays($overlapEnd) + 1; + $startOffset = $minDate->diffInDays($overlapStart); + $months[] = [ + 'name' => $tempDate->translatedFormat('F Y'), + 'left' => ($startOffset / $totalDays) * 100, + 'width' => ($daysInOverlap / $totalDays) * 100, + ]; + } + $tempDate->addMonth(); + } + + // Calculate today's line position if it falls within the range + $today = Carbon::today(); + $todayPercent = null; + if ($today->between($minDate, $maxDate)) { + $todayPercent = ($minDate->diffInDays($today) / $totalDays) * 100; + } + + $formattedInterns = $interns->map(function ($intern) use ($minDate, $totalDays) { + $start = Carbon::parse($intern->internship_start_date)->startOfDay(); + $end = Carbon::parse($intern->internship_end_date)->endOfDay(); + + $startOffset = $minDate->diffInDays($start); + $duration = $start->diffInDays($end) + 1; + + $left = ($startOffset / $totalDays) * 100; + $width = ($duration / $totalDays) * 100; + + // Ensure width is at least a visible sliver + if ($width < 1.5) { + $width = 1.5; + } + + // Assign a stable color palette based on the intern ID + $palettes = [ + [ + 'bg' => 'bg-emerald-500 dark:bg-emerald-600', + 'gradient' => 'from-emerald-400 to-teal-500 dark:from-emerald-500 dark:to-teal-600', + 'text' => 'text-emerald-950 dark:text-emerald-50', + 'border' => 'border-emerald-500/20', + 'shadow' => 'shadow-emerald-500/20' + ], + [ + 'bg' => 'bg-blue-500 dark:bg-blue-600', + 'gradient' => 'from-blue-400 to-indigo-500 dark:from-blue-500 dark:to-indigo-600', + 'text' => 'text-blue-950 dark:text-blue-50', + 'border' => 'border-blue-500/20', + 'shadow' => 'shadow-blue-500/20' + ], + [ + 'bg' => 'bg-rose-500 dark:bg-rose-600', + 'gradient' => 'from-rose-400 to-pink-500 dark:from-rose-500 dark:to-pink-600', + 'text' => 'text-rose-950 dark:text-rose-50', + 'border' => 'border-rose-500/20', + 'shadow' => 'shadow-rose-500/20' + ], + [ + 'bg' => 'bg-amber-500 dark:bg-amber-600', + 'gradient' => 'from-amber-400 to-orange-500 dark:from-amber-500 dark:to-orange-600', + 'text' => 'text-amber-950 dark:text-amber-50', + 'border' => 'border-amber-500/20', + 'shadow' => 'shadow-amber-500/20' + ], + [ + 'bg' => 'bg-violet-500 dark:bg-violet-600', + 'gradient' => 'from-violet-400 to-purple-500 dark:from-violet-500 dark:to-purple-600', + 'text' => 'text-violet-950 dark:text-violet-50', + 'border' => 'border-violet-500/20', + 'shadow' => 'shadow-violet-500/20' + ], + ]; + + $paletteIndex = $intern->id % count($palettes); + $color = $palettes[$paletteIndex]; + + return [ + 'id' => $intern->id, + 'name' => $intern->name, + 'start_date' => $start->translatedFormat('d M Y'), + 'end_date' => $end->translatedFormat('d M Y'), + 'total_days' => $intern->internship_total_days, + 'left' => $left, + 'width' => $width, + 'color' => $color, + ]; + }); + + return [ + 'interns' => $formattedInterns, + 'months' => $months, + 'todayPercent' => $todayPercent, + ]; + } +} diff --git a/resources/views/filament/admin/resources/intern-applications/widgets/intern-gantt-chart-widget.blade.php b/resources/views/filament/admin/resources/intern-applications/widgets/intern-gantt-chart-widget.blade.php new file mode 100644 index 0000000..94b8c44 --- /dev/null +++ b/resources/views/filament/admin/resources/intern-applications/widgets/intern-gantt-chart-widget.blade.php @@ -0,0 +1,134 @@ + + + +
+ + Stajyer Gantt Şeması ve Takvimi + + + Stajı Onaylananlar + +
+
+ + @if(count($interns) === 0) +
+
+ + + +
+

+ Tarihleri belirlenmiş onaylı stajyer bulunamadı. +

+
+ @else +
+
+ +
+ +
+ Stajyer Bilgisi +
+ +
+ @foreach($months as $month) +
+ {{ $month['name'] }} +
+ @endforeach +
+
+ + +
+ @foreach($interns as $intern) +
+ +
+ + {{ $intern['name'] }} + + + + + + {{ $intern['total_days'] }} Gün Staj + +
+ + +
+ + @foreach($months as $month) +
+ @endforeach + + + @if($todayPercent !== null) +
+ @endif + + +
+ + + {{ $intern['start_date'] }} + + + {{ $intern['end_date'] }} + + + + +
+
+
+ @endforeach +
+
+
+ + @if($todayPercent !== null) +
+ + Kesikli kırmızı çizgi bugünün tarihini göstermektedir. +
+ @endif + @endif +
+