feat: implement internship journal entry system with printable reports and approval workflow

This commit is contained in:
Ümit Tunç
2026-07-01 17:16:41 +03:00
parent 5b46b4ac9e
commit 4c5cf1d355
10 changed files with 984 additions and 262 deletions
@@ -187,6 +187,66 @@ class InternApplicationResource extends Resource
->columnSpanFull()
]),
Tab::make('Staj Defteri & Onay')
->icon('heroicon-o-book-open')
->schema([
\Filament\Forms\Components\Placeholder::make('notebook_view')
->label('Doldurulan Staj Defteri')
->content(function ($record) {
if (!$record) return 'Henüz başvuru bulunmamaktadır.';
$days = \App\Http\Controllers\CareerController::getInternshipDates($record->internship_start_date, $record->internship_total_days);
if (empty($days)) return 'Staj başlangıç tarihi veya süresi girilmemiş.';
$saved = $record->journalEntries()->get()->keyBy('day_number');
$html = '<div class="space-y-4" style="max-height: 400px; overflow-y: auto; padding-right: 10px; border: 1px solid #cbd5e1; border-radius: 8px; padding: 15px;">';
foreach ($days as $d) {
$dayNum = $d['day_number'];
$dateF = $d['formatted_date'];
$content = isset($saved[$dayNum]) ? $saved[$dayNum]->content : '';
$html .= '<div style="margin-bottom: 12px; padding: 12px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc;">';
$html .= ' <div style="display:flex; justify-content:between; font-size:11px; font-weight:700; color:#475569; border-bottom:1px solid #e2e8f0; padding-bottom:6px; margin-bottom:8px;">';
$html .= ' <span style="font-weight: 800; color: #2563eb;">' . $dayNum . '. Gün Raporu</span>';
$html .= ' <span style="margin-left: auto;">' . $dateF . '</span>';
$html .= ' </div>';
$html .= ' <div style="font-size:12px; color:#1e293b; white-space:pre-wrap; line-height:1.5;">' . ($content ? e($content) : '<em style="color:#94a3b8;">Rapor yazılmamış</em>') . '</div>';
$html .= '</div>';
}
$html .= '</div>';
// Add preview buttons
$html .= '<div style="margin-top: 15px; display: flex; gap: 10px;">';
$html .= ' <a href="' . route('intern.print-journal') . '?size=a4&intern_id=' . $record->id . '" target="_blank" style="display:inline-flex; align-items:center; justify-content:center; padding: 8px 16px; background:#2563eb; color:white; border-radius:8px; font-weight:bold; font-size:12px; text-decoration:none; box-shadow: 0 1px 3px rgba(37, 99, 235, 0.2);">A4 Defteri Önizle / Yazdır</a>';
$html .= ' <a href="' . route('intern.print-journal') . '?size=a5&intern_id=' . $record->id . '" target="_blank" style="display:inline-flex; align-items:center; justify-content:center; padding: 8px 16px; background:#4b5563; color:white; border-radius:8px; font-weight:bold; font-size:12px; text-decoration:none; box-shadow: 0 1px 3px rgba(75, 85, 99, 0.2);">A5 Defteri Önizle / Yazdır</a>';
$html .= '</div>';
return new \Illuminate\Support\HtmlString($html);
})
->columnSpanFull(),
\Filament\Forms\Components\Section::make('Onay ve İmza Bilgileri')
->schema([
\Filament\Forms\Components\Toggle::make('notebook_supervisor_signed')
->label('Staj Sorumlusu İmzala / Onayla')
->live(),
TextInput::make('notebook_supervisor_name')
->label('Staj Sorumlusu Adı / Ünvanı')
->placeholder('Örn: Alperen Trunç')
->default('Alperen Trunç'),
\Filament\Forms\Components\Toggle::make('notebook_unit_signed')
->label('İlgili Birim Yetkilisi İmzala / Onayla')
->live(),
TextInput::make('notebook_unit_name')
->label('Birim Yetkilisi Adı / Ünvanı')
->placeholder('Örn: Yetkili Birim Amiri')
->default('Yetkili Birim Amiri'),
\Filament\Forms\Components\Toggle::make('notebook_approved')
->label('Staj Defterini Genel Olarak Onayla')
->columnSpanFull(),
])->columns(2),
]),
Tab::make('Sertifika & Transkript')
->icon('heroicon-o-academic-cap')
->schema([
+100
View File
@@ -122,8 +122,13 @@ class CareerController extends Controller
}
$intern = CareerApplication::findOrFail(session('intern_id'));
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
$savedEntries = $intern->journalEntries()->get()->keyBy('day_number');
return view('front.career.intern_dashboard', [
'intern' => $intern,
'days' => $days,
'savedEntries' => $savedEntries,
'meta' => [
'title' => 'Stajyer Paneli',
'description' => 'Belgelerinizi buradan indirebilirsiniz.',
@@ -327,8 +332,13 @@ class CareerController extends Controller
->where('type', 'internship')
->firstOrFail();
$days = self::getInternshipDates($application->internship_start_date, $application->internship_total_days);
$savedEntries = $application->journalEntries()->get()->keyBy('day_number');
return view('front.career.verify', [
'application' => $application,
'days' => $days,
'savedEntries' => $savedEntries,
'meta' => [
'title' => 'Staj Bitirme Sertifikası Doğrulama - ' . $application->name,
'description' => $application->name . ' isimli stajyerimizin staj bitirme sertifikası ve performans raporu doğrulama sayfası.',
@@ -418,5 +428,95 @@ class CareerController extends Controller
auth()->logout();
return redirect()->route('intern.admin.login')->with('success', 'Yönetici oturumu sonlandırıldı.');
}
public static function getInternshipDates($startDate, $totalDays)
{
if (!$startDate || !$totalDays || $totalDays <= 0) {
return [];
}
$dates = [];
$temp = \Carbon\Carbon::parse($startDate);
$daysToAdd = intval($totalDays);
$count = 0;
while ($count < $daysToAdd) {
if ($temp->isWeekend()) {
$temp->addDay();
continue;
}
$dates[] = [
'day_number' => $count + 1,
'date' => $temp->format('Y-m-d'),
'formatted_date' => $temp->format('d.m.Y'),
];
$temp->addDay();
$count++;
}
return $dates;
}
public function saveJournalEntry(Request $request)
{
if (!session()->has('intern_id')) {
return response()->json(['success' => false, 'message' => 'Lütfen giriş yapın.'], 401);
}
$intern = CareerApplication::findOrFail(session('intern_id'));
$request->validate([
'day_number' => 'required|integer|min:1',
'date' => 'required|date',
'content' => 'nullable|string',
]);
$dateObj = \Carbon\Carbon::parse($request->date);
if ($dateObj->isWeekend()) {
return response()->json(['success' => false, 'message' => 'Hafta sonu günlerine staj günlüğü girilemez.'], 422);
}
$entry = \App\Models\InternshipJournalEntry::updateOrCreate([
'career_application_id' => $intern->id,
'day_number' => $request->day_number,
'date' => $request->date,
], [
'content' => $request->content,
]);
return response()->json([
'success' => true,
'message' => $request->day_number . '. Gün kaydı başarıyla kaydedildi.',
'entry' => $entry
]);
}
public function printJournal(Request $request)
{
if (request()->has('intern_id') && auth()->check() && auth()->user()->hasRole('super_admin')) {
$internId = request('intern_id');
} else {
if (!session()->has('intern_id')) {
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
}
$internId = session('intern_id');
}
$intern = CareerApplication::findOrFail($internId);
$size = $request->query('size', 'a4');
if (!in_array($size, ['a4', 'a5'])) {
$size = 'a4';
}
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
$savedEntries = $intern->journalEntries()->get()->keyBy('day_number');
return view('front.career.print', [
'intern' => $intern,
'days' => $days,
'savedEntries' => $savedEntries,
'size' => $size,
]);
}
}
+16
View File
@@ -32,8 +32,21 @@ class CareerApplication extends Model
'github_repo',
'certificate_code',
'transcript_markdown',
'notebook_supervisor_signed',
'notebook_supervisor_name',
'notebook_unit_signed',
'notebook_unit_name',
'notebook_approved',
];
/**
* Get the journal entries for the internship application.
*/
public function journalEntries()
{
return $this->hasMany(InternshipJournalEntry::class);
}
protected static function booted()
{
static::saving(function ($model) {
@@ -58,6 +71,9 @@ class CareerApplication extends Model
{
return [
'password' => 'hashed',
'notebook_supervisor_signed' => 'boolean',
'notebook_unit_signed' => 'boolean',
'notebook_approved' => 'boolean',
];
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class InternshipJournalEntry extends Model
{
use HasFactory;
protected $fillable = [
'career_application_id',
'day_number',
'date',
'content',
];
/**
* Get the career application that owns this journal entry.
*/
public function careerApplication()
{
return $this->belongsTo(CareerApplication::class);
}
}