2 Commits

10 changed files with 1007 additions and 286 deletions
@@ -107,13 +107,16 @@ class InternApplicationResource extends Resource
TextInput::make('username')
->label('Kullanıcı Adı')
->unique(ignoreRecord: true)
->nullable(),
->default(fn ($record) => $record?->email)
->disabled()
->dehydrated()
->autocomplete('new-username'),
TextInput::make('password')
->label('Şifre')
->password()
->revealable()
->autocomplete('new-password')
->formatStateUsing(fn () => null)
->dehydrateStateUsing(fn ($state) => filled($state) ? Hash::make($state) : null)
->dehydrated(fn ($state) => filled($state))
@@ -184,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,
]);
}
}
+20 -1
View File
@@ -32,17 +32,33 @@ 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) {
if ($model->type === 'internship' && !$model->certificate_code) {
if ($model->type === 'internship') {
$model->username = $model->email;
if (!$model->certificate_code) {
do {
$code = 'TRN-' . date('Y') . '-' . strtoupper(\Illuminate\Support\Str::random(4)) . '-' . strtoupper(\Illuminate\Support\Str::random(4));
} while (static::where('certificate_code', $code)->exists());
$model->certificate_code = $code;
}
}
});
}
@@ -55,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);
}
}
@@ -0,0 +1,36 @@
<?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('internship_journal_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('career_application_id')
->constrained('career_applications')
->onDelete('cascade');
$table->integer('day_number');
$table->date('date');
$table->text('content')->nullable();
$table->timestamps();
$table->unique(['career_application_id', 'day_number'], 'journal_app_day_unique');
$table->unique(['career_application_id', 'date'], 'journal_app_date_unique');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('internship_journal_entries');
}
};
@@ -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::table('career_applications', function (Blueprint $table) {
$table->boolean('notebook_supervisor_signed')->default(false);
$table->string('notebook_supervisor_name')->nullable();
$table->boolean('notebook_unit_signed')->default(false);
$table->string('notebook_unit_name')->nullable();
$table->boolean('notebook_approved')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->dropColumn([
'notebook_supervisor_signed',
'notebook_supervisor_name',
'notebook_unit_signed',
'notebook_unit_name',
'notebook_approved'
]);
});
}
};
@@ -367,14 +367,14 @@
</div>
</button>
<!-- Menu Item 3: Staj Günlüğü -->
<!-- Menu Item 3: Staj Defteri -->
<button type="button" role="tab" aria-selected="false" data-tab-target="journal" class="tab-btn w-full flex items-center gap-4 p-3 rounded-2xl text-left transition-all border border-transparent">
<div class="w-10 h-10 rounded-xl bg-slate-50 text-slate-500 flex items-center justify-center text-xl flex-shrink-0 tab-icon">
<i class="uil uil-github"></i>
<i class="uil uil-book-open"></i>
</div>
<div class="flex-grow">
<span class="block text-sm font-extrabold text-slate-800">Staj Günlüğü</span>
<span class="block text-xs text-slate-400 font-semibold mt-0.5">GitHub commit geçmişi</span>
<span class="block text-sm font-extrabold text-slate-800">Staj Defteri Doldur</span>
<span class="block text-xs text-slate-400 font-semibold mt-0.5">Günlük çalışma defteri</span>
</div>
</button>
</div>
@@ -495,27 +495,15 @@
<div class="flex items-center gap-2 w-full sm:w-auto">
@if($intern->to_be_signed_internship_form_path)
<a href="{{ Storage::disk('public')->url($intern->to_be_signed_internship_form_path) }}" target="_blank" class="px-4 py-2.5 bg-slate-850 hover:bg-slate-900 text-white font-bold rounded-xl text-xs transition-all flex items-center justify-center gap-1.5 shadow-sm">
<a href="{{ Storage::disk('public')->url($intern->to_be_signed_internship_form_path) }}" target="_blank" class="px-4 py-2.5 bg-slate-800 hover:bg-slate-900 text-white font-bold rounded-xl text-xs transition-all flex items-center justify-center gap-1.5 shadow-sm">
<i class="uil uil-arrow-down-tray"></i>
<span>Görüntüle</span>
</a>
@if(!$intern->signed_internship_form_path)
<button type="button" onclick="document.getElementById('upload-form-wrapper').classList.toggle('hidden')" class="px-4 py-2.5 bg-blue-50 hover:bg-blue-100 text-blue-600 font-bold rounded-xl text-xs transition-all flex items-center justify-center gap-1.5">
<i class="uil uil-edit"></i>
<span>Değiştir</span>
</button>
@endif
@else
<button type="button" onclick="document.getElementById('upload-form-wrapper').classList.toggle('hidden')" class="w-full sm:w-auto px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-xl text-sm transition-all flex items-center justify-center gap-2 shadow-md shadow-blue-500/10">
<i class="uil uil-plus"></i>
<span>Yükle</span>
</button>
@endif
</div>
</div>
@if(!$intern->signed_internship_form_path)
<div id="upload-form-wrapper" class="{{ $intern->to_be_signed_internship_form_path ? 'hidden' : '' }} mt-4 border-t border-slate-100 pt-4">
<div id="upload-form-wrapper" class="mt-4 border-t border-slate-100 pt-4">
<form action="{{ route('intern.upload-form') }}" method="POST" enctype="multipart/form-data" class="space-y-4">
@csrf
@@ -576,11 +564,11 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label for="internship_start_date" class="block text-xs font-extrabold text-slate-500 uppercase tracking-wider mb-1.5">Staj Başlangıç Tarihi</label>
<input type="date" name="internship_start_date" id="internship_start_date" onchange="calculateEndDateFrontend()" oninput="calculateEndDateFrontend()" required value="{{ $intern->internship_start_date }}" class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-blue-400 focus:ring-1 focus:ring-blue-400 focus:outline-none text-sm font-semibold text-slate-700 bg-white transition-all" />
<input type="date" name="internship_start_date" id="internship_start_date" onchange="calculateEndDateFrontend()" oninput="calculateEndDateFrontend()" required value="{{ $intern->internship_start_date }}" @if($intern->status !== 'pending') disabled @endif class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-blue-400 focus:ring-1 focus:ring-blue-400 focus:outline-none text-sm font-semibold text-slate-700 bg-white transition-all disabled:bg-slate-50 disabled:text-slate-500 disabled:cursor-not-allowed" />
</div>
<div>
<label for="internship_total_days" class="block text-xs font-extrabold text-slate-500 uppercase tracking-wider mb-1.5">Staj Süresi (İş Günü)</label>
<input type="number" name="internship_total_days" id="internship_total_days" onchange="calculateEndDateFrontend()" oninput="calculateEndDateFrontend()" min="1" required value="{{ $intern->internship_total_days }}" placeholder="Örn: 20" class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-blue-400 focus:ring-1 focus:ring-blue-400 focus:outline-none text-sm font-semibold text-slate-700 bg-white transition-all" />
<input type="number" name="internship_total_days" id="internship_total_days" onchange="calculateEndDateFrontend()" oninput="calculateEndDateFrontend()" min="1" required value="{{ $intern->internship_total_days }}" placeholder="Örn: 20" @if($intern->status !== 'pending') disabled @endif class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-blue-400 focus:ring-1 focus:ring-blue-400 focus:outline-none text-sm font-semibold text-slate-700 bg-white transition-all disabled:bg-slate-50 disabled:text-slate-500 disabled:cursor-not-allowed" />
</div>
</div>
<div>
@@ -589,6 +577,7 @@
</div>
</div>
@if($intern->status === 'pending')
<div class="relative border-2 border-dashed border-slate-200 hover:border-blue-400 rounded-2xl p-6 transition-all bg-white flex flex-col items-center justify-center text-center cursor-pointer group">
<input type="file" name="internship_form" id="internship_form" accept=".pdf,.docx,.jpg,.png,.jpeg" onchange="handleFileSelected(this)" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10" />
<i class="uil uil-cloud-upload text-4xl text-slate-400 group-hover:text-blue-500 transition-colors mb-2"></i>
@@ -603,9 +592,13 @@
<span>Dosyayı Kaydet</span>
</button>
</div>
</form>
@else
<div class="p-4 bg-slate-50 border border-slate-100 rounded-2xl text-center text-xs text-slate-500 font-semibold">
<i class="uil uil-lock-alt text-base mr-1"></i> Başvurunuz bekleme aşamasında (beklemede) olmadığı için staj bilgilerinizi ve dosyanızı güncelleyemezsiniz.
</div>
@endif
</form>
</div>
</div>
<!-- Signed Internship Form Card -->
@@ -626,9 +619,7 @@
@if($intern->signed_internship_form_path)
<a href="{{ Storage::disk('public')->url($intern->signed_internship_form_path) }}" target="_blank" class="w-full sm:w-auto px-5 py-2.5 bg-[#e31e24] hover:bg-[#c4191f] text-white font-bold rounded-xl text-sm transition-all flex items-center justify-center gap-2 shadow-md shadow-red-500/10">
<i class="uil uil-arrow-down-tray"></i>
<span>İndir</span>
</a>
</div>
@else
<button disabled class="w-full sm:w-auto px-5 py-2.5 bg-slate-100 text-slate-400 font-bold rounded-xl text-sm cursor-not-allowed flex items-center justify-center gap-2 border border-slate-200/50">
<i class="uil uil-hourglass"></i>
@@ -641,20 +632,21 @@
</div>
</div>
<!-- Panel 3: Staj Günlüğü -->
<!-- Panel 3: Staj Defteri -->
<div id="journal-panel" class="tab-panel hidden space-y-6">
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6 pb-4 border-b border-slate-100">
<div>
<h3 class="font-bold text-slate-800 text-lg">Staj Günlüğü</h3>
<p class="text-xs text-slate-400 mt-0.5">Public GitHub deponuzdaki commit geçmişi günlük olarak listelenir.</p>
<h3 class="font-bold text-slate-800 text-lg">Staj Defteri Modülü</h3>
<p class="text-xs text-slate-400 mt-0.5">Her staj günü için yaptığınız çalışmaları yazıp kaydedin. Dilerseniz GitHub commitlerini tek tıkla çekebilirsiniz.</p>
</div>
<div class="flex gap-2 w-full sm:w-auto">
<button type="button" onclick="openPrintModal()" class="w-full sm:w-auto px-4 py-2.5 bg-green-600 hover:bg-green-700 text-white font-bold rounded-xl text-xs transition-all flex items-center justify-center gap-2 shadow-md shadow-green-500/10">
<i class="uil uil-print text-sm"></i>
<span>Defteri Yazdır / İndir</span>
</button>
</div>
@if($intern->github_repo)
<a href="{{ route('intern.download-journal') }}" class="w-full sm:w-auto px-4 py-2.5 bg-green-600 hover:bg-green-700 text-white font-bold rounded-xl text-xs transition-all flex items-center justify-center gap-2 shadow-md shadow-green-500/10">
<i class="uil uil-arrow-down-tray text-sm"></i>
<span>Günlüğü İndir (.md)</span>
</a>
@endif
</div>
<!-- GitHub Repository URL Configuration -->
@@ -669,61 +661,82 @@
{{ $intern->github_repo ? 'Güncelle' : 'Kaydet' }}
</button>
</div>
<span class="block text-xs text-slate-400 mt-2"><i class="uil uil-info-circle text-blue-600"></i> Deponuzun <strong>public (herkese açık)</strong> olması ve en az bir commit içermesi gerekmektedir.</span>
<span class="block text-xs text-slate-400 mt-2"><i class="uil uil-info-circle text-blue-600"></i> Deponuzun <strong>public (herkese açık)</strong> olması gerekmektedir.</span>
</div>
</form>
</div>
<!-- Commits timeline / history load wrapper -->
<div id="commits-loader" class="hidden py-8 flex flex-col items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mb-2"></div>
<span class="text-xs text-slate-400 font-semibold">GitHub'dan commitler yükleniyor...</span>
@if(empty($intern->internship_start_date) || empty($intern->internship_total_days))
<div class="p-6 rounded-2xl border border-amber-100 bg-amber-50 text-amber-800 text-sm font-medium text-center">
Staj başlangıç tarihi veya staj gün sayısı girilmemiş. Lütfen staj evraklarınızı yükleyip başlangıç tarihlerinizi tanımlayın.
</div>
@else
<!-- Main Workspace Grid -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<div id="commits-error" class="hidden p-4 rounded-xl bg-red-50 border border-red-200 text-[#e31e24] text-xs font-semibold">
<!-- Left: Days List Navigation -->
<div class="lg:col-span-4 space-y-2 max-h-[500px] overflow-y-auto pr-2 no-scrollbar">
@foreach($days as $idx => $d)
@php
$dayNum = $d['day_number'];
$dayDate = $d['date'];
$dayDateFormatted = $d['formatted_date'];
$hasSaved = isset($savedEntries[$dayNum]) && trim($savedEntries[$dayNum]->content) !== '';
@endphp
<button type="button"
onclick="selectNotebookDay({{ $idx }})"
id="btn-day-{{ $idx }}"
data-day-num="{{ $dayNum }}"
data-date="{{ $dayDate }}"
data-formatted-date="{{ $dayDateFormatted }}"
data-saved-content="{{ $savedEntries[$dayNum]->content ?? '' }}"
class="notebook-day-btn w-full flex items-center justify-between p-3 rounded-2xl border text-left transition-all @if($idx === 0) border-blue-200 bg-blue-50/50 @else border-slate-100 bg-white hover:bg-slate-50 @endif">
<div>
<span class="block text-xs font-extrabold @if($idx === 0) text-blue-700 @else text-slate-800 @endif">{{ $dayNum }}. Gün</span>
<span class="block text-[10px] text-slate-400 mt-0.5">{{ $dayDateFormatted }}</span>
</div>
<div id="commits-empty" class="hidden p-6 rounded-2xl border border-slate-100 text-center text-slate-400 italic text-sm">
Henüz hiç commit bulunamadı veya deponuz tanımlanmadı. Lütfen deponuzu tanımlayın ve commitlerinizi gönderin.
</div>
<!-- Day Paginator / Navbar -->
<div id="journal-carousel-nav" class="hidden flex items-center justify-between gap-4 p-3 bg-slate-50/80 border border-slate-100 rounded-2xl mb-6 select-none">
<button type="button" id="prev-day-btn" onclick="navigateDay(-1)" class="w-10 h-10 rounded-xl bg-white hover:bg-blue-50 text-slate-600 hover:text-blue-600 border border-slate-200/60 hover:border-blue-100 flex items-center justify-center transition-all shadow-sm">
<i class="uil uil-angle-left text-xl"></i>
<span id="badge-day-{{ $idx }}" class="inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase @if($hasSaved) bg-green-50 text-green-700 border border-green-200 @else bg-slate-50 text-slate-400 border border-slate-200 @endif">
{{ $hasSaved ? 'DOLU' : 'BOŞ' }}
</span>
</button>
<div class="flex-grow overflow-x-auto no-scrollbar py-1">
<div id="day-tabs-container" class="flex gap-2 justify-start sm:justify-center min-w-max px-2">
<!-- Day tabs rendered dynamically -->
</div>
@endforeach
</div>
<button type="button" id="next-day-btn" onclick="navigateDay(1)" class="w-10 h-10 rounded-xl bg-white hover:bg-blue-50 text-slate-600 hover:text-blue-600 border border-slate-200/60 hover:border-blue-100 flex items-center justify-center transition-all shadow-sm">
<i class="uil uil-angle-right text-xl"></i>
<!-- Right: Workspace Editor -->
<div class="lg:col-span-8 space-y-4">
<div class="bg-white border border-slate-100 rounded-2xl p-5 shadow-sm">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 mb-4">
<div>
<span id="editor-day-title" class="text-sm font-extrabold text-blue-600 bg-blue-50 px-3 py-1 rounded-full">1. Gün</span>
<span id="editor-day-date" class="text-xs font-semibold text-slate-400 ml-2"></span>
</div>
@if($intern->github_repo)
<button type="button" onclick="fillFromGithub()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-900 text-white font-extrabold rounded-lg text-[10px] transition-all flex items-center gap-1.5 shadow-sm">
<i class="uil uil-github text-xs"></i>
<span>Git Commitlerini Çek</span>
</button>
@endif
</div>
<div class="space-y-4">
<div>
<label class="block text-xs font-extrabold text-slate-500 uppercase tracking-wider mb-2">Günlük Çalışma Raporu</label>
<textarea id="editor-content" rows="10" placeholder="Bu staj gününde yaptığınız çalışmaları detaylıca yazın..." class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-blue-400 focus:ring-1 focus:ring-blue-400 focus:outline-none text-sm font-semibold text-slate-700 bg-white transition-all"></textarea>
</div>
<div class="flex justify-between items-center">
<div id="save-status" class="text-xs font-semibold text-slate-400"></div>
<button type="button" onclick="saveActiveDay()" class="px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-xl text-xs transition-all shadow-md shadow-blue-500/10">
Deftere Kaydet
</button>
</div>
<!-- Day Content Timeline Container (Carousel View) -->
<div id="commits-timeline-container" class="hidden">
<div id="active-day-card" class="bg-white border border-slate-100/80 rounded-2xl p-6 shadow-sm transition-all duration-300 transform">
<!-- Active Day Details (Date / Day Title) -->
<div class="flex items-center justify-between mb-8 pb-4 border-b border-slate-100">
<div class="flex items-center gap-3">
<span id="active-day-title" class="text-base font-extrabold text-blue-600 bg-blue-50 px-4 py-1.5 rounded-full">1. Gün</span>
<span id="active-day-date" class="text-sm font-semibold text-slate-400"><i class="uil uil-calendar-alt mr-1"></i> 29.05.2026</span>
</div>
<span id="active-day-commit-count" class="text-xs font-bold text-slate-400 bg-slate-50 border border-slate-100 px-2.5 py-1 rounded-lg">3 Commit</span>
</div>
</div>
<!-- Timeline items list -->
<div class="relative pl-6 sm:pl-8 before:content-[''] before:absolute before:left-[11px] sm:before:left-[15px] before:top-2 before:bottom-2 before:w-[2px] before:bg-slate-100">
<div id="timeline-events" class="space-y-6">
<!-- Event items rendered dynamically -->
</div>
</div>
</div>
</div>
@endif
</div>
</div>
@@ -736,6 +749,43 @@
</div>
</section>
<!-- Print Choice Modal -->
<div id="print-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
<div class="bg-white rounded-3xl p-6 max-w-md w-full shadow-2xl border border-slate-100/50 transform scale-95 opacity-0 transition-all duration-300" id="print-modal-card">
<div class="flex justify-between items-start mb-6">
<div>
<h3 class="text-lg font-bold text-slate-800">Staj Defteri Yazdır / İndir</h3>
<p class="text-xs text-slate-400 mt-1">Lütfen yazdırmak istediğiniz staj defterinin sayfa boyutunu seçin.</p>
</div>
<button onclick="closePrintModal()" class="text-slate-400 hover:text-slate-600">
<i class="uil uil-multiply text-xl"></i>
</button>
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<a href="{{ route('intern.print-journal') }}?size=a4" target="_blank" onclick="closePrintModal()" class="flex flex-col items-center justify-center p-5 rounded-2xl border border-slate-100 bg-slate-50/50 hover:border-blue-300 hover:bg-blue-50/20 transition-all group text-center">
<div class="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center text-2xl mb-3 group-hover:bg-blue-600 group-hover:text-white transition-all">
<i class="uil uil-file-alt"></i>
</div>
<span class="block text-sm font-extrabold text-slate-800">A4 Boyutu</span>
<span class="block text-[10px] text-slate-400 mt-0.5">Dikey (210 x 297 mm)</span>
</a>
<a href="{{ route('intern.print-journal') }}?size=a5" target="_blank" onclick="closePrintModal()" class="flex flex-col items-center justify-center p-5 rounded-2xl border border-slate-100 bg-slate-50/50 hover:border-blue-300 hover:bg-blue-50/20 transition-all group text-center">
<div class="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center text-2xl mb-3 group-hover:bg-blue-600 group-hover:text-white transition-all">
<i class="uil uil-file"></i>
</div>
<span class="block text-sm font-extrabold text-slate-800">A5 Boyutu</span>
<span class="block text-[10px] text-slate-400 mt-0.5">Dikey (148 x 210 mm)</span>
</a>
</div>
<div class="p-4 bg-blue-50 border border-blue-100 rounded-2xl text-xs text-blue-700 font-semibold leading-relaxed">
<i class="uil uil-info-circle text-sm mr-1"></i> Açılan önizleme sayfasında <strong>CTRL + P</strong> tuşlarına basarak "Hedef: PDF Olarak Kaydet" seçeneği ile staj defterinizi bilgisayarınıza indirebilirsiniz.
</div>
</div>
</div>
@push('styles')
<script src="https://cdn.tailwindcss.com"></script>
<script>
@@ -773,226 +823,53 @@
if (targetPanel) {
targetPanel.classList.remove('hidden');
}
// If journal is selected, trigger GitHub commits loading
if (targetId === 'journal') {
loadGithubCommits();
}
});
});
// Handle initial state of date logic
calculateEndDateFrontend();
// Fetch Github commits in memory if repo is set
fetchAllCommitsInMemory();
// Select first day of the notebook workspace on load
selectNotebookDay(0);
});
const githubRepoUrl = @json($intern->github_repo);
let commitDays = [];
let activeDayIndex = 0;
let allGithubCommits = {};
let activeDayIdx = 0;
function loadGithubCommits() {
const loader = document.getElementById('commits-loader');
const errorEl = document.getElementById('commits-error');
const emptyEl = document.getElementById('commits-empty');
const navEl = document.getElementById('journal-carousel-nav');
const containerEl = document.getElementById('commits-timeline-container');
function fetchAllCommitsInMemory() {
if (!githubRepoUrl) return;
if (!githubRepoUrl) {
emptyEl.classList.remove('hidden');
return;
}
loader.classList.remove('hidden');
errorEl.classList.add('hidden');
emptyEl.classList.add('hidden');
navEl.classList.add('hidden');
containerEl.classList.add('hidden');
commitDays = [];
// Parse owner and repo
let repoClean = githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
repoClean = repoClean.replace(/\/$/, '');
const parts = repoClean.split('/');
if (parts.length < 2) {
loader.classList.add('hidden');
errorEl.textContent = 'Github depo URL\'si çözümlenemedi. Geçerli format: https://github.com/kullanici/depo';
errorEl.classList.remove('hidden');
return;
}
if (parts.length < 2) return;
const owner = parts[0];
const repo = parts[1].replace(/\.git$/i, '');
fetch(`https://api.github.com/repos/${owner}/${repo}/commits?per_page=100`)
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('Depo bulunamadı veya private (gizli). Deponuzun public (herkese açık) olduğundan emin olun.');
} else if (response.status === 403) {
throw new Error('GitHub API istek limiti aşıldı. Lütfen daha sonra tekrar deneyin veya yukarıdaki indirme butonunu kullanın.');
} else {
throw new Error('GitHub API hatası: ' + response.statusText);
}
}
if (!response.ok) throw new Error('API Error');
return response.json();
})
.then(commits => {
loader.classList.add('hidden');
if (!Array.isArray(commits) || commits.length === 0) {
emptyEl.classList.remove('hidden');
return;
}
// Reverse to show in chronological order (Day 1, Day 2...)
commits.reverse();
// Group commits by date (YYYY-MM-DD)
const grouped = {};
if (!Array.isArray(commits)) return;
commits.forEach(item => {
const dateStr = item.commit.author.date;
if (dateStr) {
const dateOnly = dateStr.substring(0, 10); // "YYYY-MM-DD"
if (!grouped[dateOnly]) {
grouped[dateOnly] = [];
const dateOnly = dateStr.substring(0, 10);
if (!allGithubCommits[dateOnly]) {
allGithubCommits[dateOnly] = [];
}
grouped[dateOnly].push(item);
allGithubCommits[dateOnly].push(item);
}
});
const sortedDates = Object.keys(grouped).sort();
commitDays = sortedDates.map((date, index) => {
const dateParts = date.split('-');
return {
date: date,
dayNum: index + 1,
formattedDate: `${dateParts[2]}.${dateParts[1]}.${dateParts[0]}`,
commits: grouped[date]
};
});
if (commitDays.length === 0) {
emptyEl.classList.remove('hidden');
return;
}
activeDayIndex = 0;
navEl.classList.remove('hidden');
containerEl.classList.remove('hidden');
renderDayTabs();
renderActiveDay();
})
.catch(err => {
loader.classList.add('hidden');
errorEl.textContent = err.message || 'Bir hata oluştu.';
errorEl.classList.remove('hidden');
});
}
function renderDayTabs() {
const container = document.getElementById('day-tabs-container');
container.innerHTML = '';
commitDays.forEach((day, index) => {
const isActive = index === activeDayIndex;
const activeClass = isActive
? 'text-blue-700 bg-blue-50 border border-blue-200 font-extrabold shadow-sm'
: 'bg-white hover:bg-slate-50 text-slate-600 border border-slate-200/60 font-semibold hover:text-blue-600';
const tab = document.createElement('button');
tab.type = 'button';
tab.className = `px-5 py-2 rounded-xl transition-all flex flex-col items-center justify-center ${activeClass}`;
tab.innerHTML = `
<span class="text-xs">${day.dayNum}. Gün</span>
<span class="text-[9px] mt-0.5 opacity-70 font-medium">${day.formattedDate}</span>
`;
tab.onclick = () => {
activeDayIndex = index;
renderDayTabs();
renderActiveDay();
};
container.appendChild(tab);
});
// Handle scroll to active element
const activeTab = container.children[activeDayIndex];
if (activeTab) {
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
// Disable/enable arrows
const prevBtn = document.getElementById('prev-day-btn');
const nextBtn = document.getElementById('next-day-btn');
prevBtn.disabled = activeDayIndex === 0;
prevBtn.classList.toggle('opacity-40', activeDayIndex === 0);
prevBtn.classList.toggle('cursor-not-allowed', activeDayIndex === 0);
nextBtn.disabled = activeDayIndex === commitDays.length - 1;
nextBtn.classList.toggle('opacity-40', activeDayIndex === commitDays.length - 1);
nextBtn.classList.toggle('cursor-not-allowed', activeDayIndex === commitDays.length - 1);
}
function renderActiveDay() {
const card = document.getElementById('active-day-card');
// Smooth transition
card.style.opacity = '0.3';
card.style.transform = 'translateY(5px)';
setTimeout(() => {
const day = commitDays[activeDayIndex];
document.getElementById('active-day-title').textContent = `${day.dayNum}. Gün`;
document.getElementById('active-day-date').innerHTML = `<i class="uil uil-calendar-alt mr-1"></i> ${day.formattedDate}`;
document.getElementById('active-day-commit-count').textContent = `${day.commits.length} Commit`;
const eventsContainer = document.getElementById('timeline-events');
eventsContainer.innerHTML = '';
day.commits.forEach(c => {
const message = c.commit.message;
const authorDate = new Date(c.commit.author.date);
const timeStr = authorDate.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
const shaShort = c.sha.substring(0, 7);
const commitUrl = c.html_url;
const eventHtml = `
<div class="relative group">
<!-- Timeline Dot -->
<div class="absolute -left-[31px] sm:-left-[39px] top-1.5 w-6 h-6 rounded-full bg-white border-2 border-blue-500 flex items-center justify-center transition-all group-hover:bg-blue-500">
<div class="w-2 h-2 rounded-full bg-blue-500 group-hover:bg-white transition-all"></div>
</div>
<!-- Event Card -->
<div class="p-4 bg-slate-50/50 hover:bg-blue-50/10 border border-slate-100 hover:border-blue-100/60 rounded-2xl transition-all shadow-sm">
<div class="flex items-center justify-between gap-4 mb-2">
<span class="text-xs font-bold text-slate-400"><i class="uil uil-clock mr-1"></i> ${timeStr}</span>
<a href="${commitUrl}" target="_blank" class="text-xs font-mono font-bold text-blue-600 hover:text-blue-700 bg-blue-50/60 hover:bg-blue-50 px-2.5 py-0.5 rounded-lg border border-blue-100/40 transition-colors">
${shaShort}
</a>
</div>
<p class="text-sm font-bold text-slate-700 leading-relaxed whitespace-pre-line">${escapeHtml(message)}</p>
</div>
</div>
`;
eventsContainer.insertAdjacentHTML('beforeend', eventHtml);
});
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, 150);
}
function navigateDay(dir) {
const newIndex = activeDayIndex + dir;
if (newIndex >= 0 && newIndex < commitDays.length) {
activeDayIndex = newIndex;
renderDayTabs();
renderActiveDay();
}
}
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
@@ -0,0 +1,445 @@
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Staj Defteri - {{ $intern->name }}</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Outfit', 'Segoe UI', sans-serif;
color: #1e293b;
background-color: #f8fafc;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* Page setup depending on chosen size */
@page {
@if($size === 'a5')
size: A5 portrait;
margin: 10mm;
@else
size: A4 portrait;
margin: 15mm;
@endif
}
.notebook-page {
background-color: #ffffff;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
page-break-after: always;
break-after: page;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
@if($size === 'a5')
width: 148mm;
height: 210mm;
padding: 12mm;
font-size: 11px;
@else
width: 210mm;
height: 297mm;
padding: 20mm;
font-size: 13px;
@endif
margin: 20px auto;
}
/* Adjustments for actual print mode */
@media print {
body {
background-color: #ffffff;
}
.notebook-page {
border: none;
box-shadow: none;
margin: 0;
width: 100%;
height: 100%;
}
.no-print {
display: none !important;
}
}
/* Top control bar */
.control-bar {
background: #ffffff;
border-bottom: 1px solid #e2e8f0;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border-radius: 10px;
font-size: 13px;
font-weight: 700;
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.btn-primary {
background-color: #2563eb;
color: #ffffff;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-secondary {
background-color: #f1f5f9;
color: #475569;
border-color: #cbd5e1;
}
.btn-secondary:hover {
background-color: #e2e8f0;
}
.control-title {
font-weight: 800;
font-size: 16px;
color: #0f172a;
}
.control-actions {
display: flex;
gap: 10px;
}
/* Page Layout Styles */
.header-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
.header-table td {
border: 1px solid #cbd5e1;
padding: 8px 12px;
vertical-align: middle;
}
.logo-cell {
width: 75px;
text-align: center;
}
.logo-img {
max-height: 40px;
max-width: 100%;
}
.company-title {
font-weight: 800;
font-size: 12px;
color: #0f172a;
line-height: 1.3;
text-transform: uppercase;
}
.doc-title {
font-weight: 800;
color: #2563eb;
text-align: right;
font-size: 14px;
}
.meta-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.meta-table td {
border: 1px solid #cbd5e1;
padding: 6px 10px;
font-size: 11px;
}
.meta-label {
font-weight: 700;
color: #475569;
background-color: #f8fafc;
width: 25%;
}
.meta-value {
font-weight: 600;
color: #0f172a;
}
.day-banner {
background: linear-gradient(135deg, #eff6ff, #dbeafe);
border: 1px solid #bfdbfe;
border-radius: 8px;
padding: 10px 15px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.day-badge {
font-weight: 800;
color: #1d4ed8;
font-size: 14px;
}
.day-date {
font-weight: 700;
color: #475569;
font-size: 12px;
}
.content-area {
flex-grow: 1;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 20px;
background: #ffffff;
margin-bottom: 20px;
min-height: 250px;
white-space: pre-wrap;
line-height: 1.6;
color: #334155;
font-size: 13px;
}
.empty-content {
color: #94a3b8;
font-style: italic;
text-align: center;
padding-top: 50px;
}
.footer-section {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-top: auto;
border-t: 1px solid #e2e8f0;
padding-top: 15px;
}
.qr-block {
display: flex;
align-items: center;
gap: 10px;
}
.qr-img {
width: 65px;
height: 65px;
border: 1px solid #e2e8f0;
padding: 2px;
border-radius: 4px;
}
.qr-text {
font-size: 9px;
color: #64748b;
max-width: 120px;
line-height: 1.3;
font-weight: 500;
}
.signatures-container {
display: flex;
gap: 20px;
}
.signature-box {
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 8px 12px;
width: 150px;
text-align: center;
font-size: 9px;
background: #f8fafc;
min-height: 70px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.sig-title {
font-weight: 700;
color: #475569;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 4px;
margin-bottom: 4px;
text-transform: uppercase;
}
.sig-status {
font-weight: 800;
color: #059669;
margin: 6px 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.sig-status.waiting {
color: #d97706;
}
.sig-badge {
background-color: #d1fae5;
color: #065f46;
padding: 1px 6px;
border-radius: 4px;
font-size: 7px;
font-weight: 800;
display: inline-block;
margin-top: 2px;
}
.sig-badge.waiting {
background-color: #fef3c7;
color: #92400e;
}
.sig-name {
font-weight: 700;
color: #1e293b;
}
</style>
</head>
<body>
<!-- Control Bar -->
<div class="control-bar no-print">
<div class="control-title">Staj Defteri Önizleme ({{ $size === 'a5' ? 'A5 Boyutu' : 'A4 Boyutu' }})</div>
<div class="control-actions">
<a href="{{ route('intern.dashboard') }}" class="btn btn-secondary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
<span>Panele Dön</span>
</a>
<div style="border-left: 1px solid #e2e8f0; margin: 0 5px; height: 35px;"></div>
<a href="?size=a4{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}" class="btn {{ $size === 'a4' ? 'btn-primary' : 'btn-secondary' }}">A4 Boyutu</a>
<a href="?size=a5{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}" class="btn {{ $size === 'a5' ? 'btn-primary' : 'btn-secondary' }}">A5 Boyutu</a>
<button onclick="window.print()" class="btn btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
<span>Yazdır / PDF Kaydet</span>
</button>
</div>
</div>
<!-- Render Each Day -->
@foreach($days as $day)
@php
$entry = $savedEntries->get($day['day_number']);
$dayNum = $day['day_number'];
$dayDate = $day['date'];
$dayDateFormatted = $day['formatted_date'];
// Build the verification QR code URL pointing to the verification page
$verifyUrl = route('internship.verify', $intern->certificate_code) . '?day=' . $dayNum;
$qrCodeUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=' . urlencode($verifyUrl);
@endphp
<div class="notebook-page">
<div>
<!-- Header Card -->
<table class="header-table">
<tr>
<td class="logo-cell">
<img src="{{ asset('assets/img/logo.png') }}" class="logo-img" alt="Logo" onerror="this.src='https://www.truncgil.com/assets/images/logo/logo.svg'">
</td>
<td>
<div class="company-title">TRUNÇGİL TEKNOLOJİ</div>
<div style="font-size: 8px; color: #64748b; margin-top: 2px;">Gaziantep Teknopark, Şahinbey / Gaziantep</div>
</td>
<td style="text-align: right;">
<div class="doc-title">STAJ GÜNLÜK RAPORU</div>
<div style="font-size: 8px; color: #64748b; margin-top: 2px;">Staj Defteri Sayfası</div>
</td>
</tr>
</table>
<!-- Student & Metadata Grid -->
<table class="meta-table">
<tr>
<td class="meta-label">Öğrenci Ad Soyad</td>
<td class="meta-value">{{ $intern->name }}</td>
<td class="meta-label">Staj Türü / Deposu</td>
<td class="meta-value">{{ $intern->github_repo ? 'GitHub Projeli Staj' : 'Genel Staj' }}</td>
</tr>
<tr>
<td class="meta-label">Staj Başlangıç</td>
<td class="meta-value">{{ \Carbon\Carbon::parse($intern->internship_start_date)->format('d.m.Y') }}</td>
<td class="meta-label">Toplam İş Günü</td>
<td class="meta-value">{{ $intern->internship_total_days }} Gün</td>
</tr>
</table>
<!-- Day Header Banner -->
<div class="day-banner">
<span class="day-badge">{{ $dayNum }}. Gün Raporu</span>
<span class="day-date"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block; vertical-align:middle; margin-right:4px;"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>{{ $dayDateFormatted }}</span>
</div>
<!-- Content Area -->
<div class="content-area">
@if($entry && trim($entry->content))
{!! nl2br(e($entry->content)) !!}
@else
<div class="empty-content">Bu gün için herhangi bir staj raporu girilmemiştir.</div>
@endif
</div>
</div>
<!-- Footer & Signatures Block -->
<div class="footer-section">
<!-- QR Code Block -->
<div class="qr-block">
<img src="{{ $qrCodeUrl }}" class="qr-img" alt="QR Code">
<div class="qr-text">
<strong>KOD DOĞRULAMA</strong><br>
Bu staj sayfası webden doğrulanabilir. Doğrulamak için QR kodu taratın.
</div>
</div>
<!-- Signatures -->
<div class="signatures-container">
<!-- Supervisor -->
<div class="signature-box">
<div class="sig-title">Staj Sorumlusu</div>
@if($intern->notebook_supervisor_signed)
<div class="sig-status">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color: #059669;"><polyline points="20 6 9 17 4 12"/></svg>
<span class="sig-badge">E-ONAYLI</span>
</div>
<div class="sig-name">{{ $intern->notebook_supervisor_name ?: 'Alperen Trunç' }}</div>
@else
<div class="sig-status waiting">
<span class="sig-badge waiting">İMZA BEKLENİYOR</span>
</div>
<div class="sig-name">&nbsp;</div>
@endif
</div>
<!-- Unit Officer -->
<div class="signature-box">
<div class="sig-title">Birim Yetkilisi</div>
@if($intern->notebook_unit_signed)
<div class="sig-status">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color: #059669;"><polyline points="20 6 9 17 4 12"/></svg>
<span class="sig-badge">E-ONAYLI</span>
</div>
<div class="sig-name">{{ $intern->notebook_unit_name ?: 'Yetkili Birim Amiri' }}</div>
@else
<div class="sig-status waiting">
<span class="sig-badge waiting">İMZA BEKLENİYOR</span>
</div>
<div class="sig-name">&nbsp;</div>
@endif
</div>
</div>
</div>
</div>
@endforeach
</body>
</html>
@@ -25,6 +25,84 @@
</button>
</div>
</div>
<!-- ================= PAGE 3: STAJ DEFTERİ (PORTRAIT A4) ================= -->
@if($application->notebook_approved || $application->notebook_supervisor_signed || $application->notebook_unit_signed)
<div class="notebook-container-wrapper overflow-hidden w-full flex justify-center items-start mt-8">
<div id="notebook-node" class="transcript-page bg-white rounded-[2rem] shadow-2xl p-6 md:p-8 relative overflow-hidden mx-auto">
<!-- Thin border decor for notebook -->
<div class="absolute inset-4 border border-slate-100 rounded-[1.5rem] pointer-events-none"></div>
<div class="relative z-10 flex flex-col justify-between h-full text-slate-700">
<div>
<!-- Header -->
<div class="flex justify-between items-center pb-2 border-b border-slate-100 mb-3">
<div>
<h2 class="font-serif font-bold text-slate-900 transcript-header-title">ONAYLI STAJ DEFTERİ RAPORU</h2>
<p class="text-slate-400 tracking-wider mt-0.5 transcript-header-subtitle">DİJİTAL İMZALI VE ONAYLI STAJ RAPORLARI</p>
</div>
<img src="{{ asset('logos/truncgil-yatay.svg') }}" alt="Trunçgil Logo" class="h-6 object-contain">
</div>
<!-- Signatures Information -->
<div class="bg-emerald-50 border border-emerald-200 rounded-xl p-3 mb-4 flex justify-between items-center">
<div>
<span class="text-xs font-extrabold text-emerald-800 uppercase block tracking-wider">Dijital İmza Durumu</span>
<span class="text-[10px] text-emerald-600 font-semibold block mt-0.5">Bu staj defteri yetkililer tarafından elektronik olarak imzalanmıştır.</span>
</div>
<div class="flex gap-2">
@if($application->notebook_supervisor_signed)
<span class="inline-flex px-2 py-1 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">
Sorumlu: {{ $application->notebook_supervisor_name ?: 'Alperen Trunç' }}
</span>
@endif
@if($application->notebook_unit_signed)
<span class="inline-flex px-2 py-1 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">
Yetkili: {{ $application->notebook_unit_name ?: 'Birim Yetkilisi' }}
</span>
@endif
</div>
</div>
<!-- Notebook Days content overview -->
<div class="overflow-y-auto max-h-[700px] pr-2 no-scrollbar space-y-4">
@foreach($days as $day)
@php
$entry = $savedEntries->get($day['day_number']);
@endphp
@if($entry && trim($entry->content))
<div class="p-3 bg-slate-50 border border-slate-100 rounded-xl">
<div class="flex justify-between items-center pb-1.5 border-b border-slate-200/50 mb-1.5">
<span class="text-xs font-extrabold text-slate-800">{{ $day['day_number'] }}. Gün Raporu</span>
<span class="text-[10px] text-slate-400 font-bold">{{ $day['formatted_date'] }}</span>
</div>
<p class="text-xs text-slate-600 whitespace-pre-wrap leading-relaxed" style="font-size: 11px;">{{ $entry->content }}</p>
</div>
@endif
@endforeach
</div>
</div>
<!-- Footer -->
<div class="w-full flex justify-between items-center pt-2 border-t border-slate-100 gap-4 mt-auto">
<div class="flex items-center gap-3">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data={{ urlencode(route('internship.verify', $application->certificate_code)) }}" alt="QR Code" class="w-14 h-14 border border-slate-200 p-0.5 rounded-lg bg-white">
<div>
<span class="font-bold text-slate-400 tracking-wider block verify-code-label">GÜVENLİ DOĞRULAMA KODU</span>
<span class="font-mono font-bold text-slate-800 block verify-code">{{ $application->certificate_code }}</span>
</div>
</div>
<div class="text-right text-slate-400 footer-address">
Trunçgil Teknoloji San. ve Tic. Ltd. Şti.<br>
Gaziantep Üniversitesi Teknopark No: 4A
</div>
</div>
</div>
</div>
</div>
@endif
</div>
</div>
@@ -426,6 +504,7 @@
function scaleAllDocuments() {
scaleDocument('.certificate-container-wrapper', '#certificate-node', 1122, 794);
scaleDocument('.transcript-container-wrapper', '#transcript-node', 794, 1122);
scaleDocument('.notebook-container-wrapper', '#notebook-node', 794, 1122);
}
window.addEventListener('resize', scaleAllDocuments);
@@ -455,6 +534,7 @@
const certEl = document.getElementById('certificate-node');
const transEl = document.getElementById('transcript-node');
const notebookEl = document.getElementById('notebook-node');
// Force elements to normal scale prior to capturing for PDF
const origCertTransform = certEl.style.transform;
@@ -466,6 +546,19 @@
const origTransHeight = transEl.style.height;
const origTransRadius = transEl.style.borderRadius;
let origNotebookTransform, origNotebookWidth, origNotebookHeight, origNotebookRadius;
if (notebookEl) {
origNotebookTransform = notebookEl.style.transform;
origNotebookWidth = notebookEl.style.width;
origNotebookHeight = notebookEl.style.height;
origNotebookRadius = notebookEl.style.borderRadius;
notebookEl.style.transform = 'none';
notebookEl.style.width = '794px';
notebookEl.style.height = '1122px';
notebookEl.style.borderRadius = '0';
}
certEl.style.transform = 'none';
certEl.style.width = '1122px';
certEl.style.height = '794px';
@@ -509,6 +602,21 @@
pdf.addPage('a4', 'portrait');
pdf.addImage(transImgData, 'JPEG', 0, 0, transWidth, transHeight);
// Render Page 3 (Notebook) if it exists
if (notebookEl) {
const notebookCanvas = await html2canvas(notebookEl, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
logging: false
});
const notebookImgData = notebookCanvas.toDataURL('image/jpeg', 1.0);
const notebookHeight = (notebookCanvas.height * transWidth) / notebookCanvas.width;
pdf.addPage('a4', 'portrait');
pdf.addImage(notebookImgData, 'JPEG', 0, 0, transWidth, notebookHeight);
}
// Restore styles
certEl.style.transform = origCertTransform;
certEl.style.width = origCertWidth;
@@ -519,6 +627,13 @@
transEl.style.height = origTransHeight;
transEl.style.borderRadius = origTransRadius;
if (notebookEl) {
notebookEl.style.transform = origNotebookTransform;
notebookEl.style.width = origNotebookWidth;
notebookEl.style.height = origNotebookHeight;
notebookEl.style.borderRadius = origNotebookRadius;
}
// Trigger Save File
pdf.save('{{ str()->slug($application->name) }}-staj-belgesi.pdf');
+2
View File
@@ -122,6 +122,8 @@ Route::get('/stajyer/panel', [\App\Http\Controllers\CareerController::class, 'in
Route::post('/stajyer/form-yukle', [\App\Http\Controllers\CareerController::class, 'uploadInternshipForm'])->name('intern.upload-form');
Route::post('/stajyer/github-kaydet', [\App\Http\Controllers\CareerController::class, 'saveGithubRepo'])->name('intern.save-github');
Route::get('/stajyer/gunluk-indir', [\App\Http\Controllers\CareerController::class, 'downloadMarkdown'])->name('intern.download-journal');
Route::post('/stajyer/defteri-kaydet', [\App\Http\Controllers\CareerController::class, 'saveJournalEntry'])->name('intern.save-journal');
Route::get('/stajyer/defteri-yazdir', [\App\Http\Controllers\CareerController::class, 'printJournal'])->name('intern.print-journal');
Route::get('/staj-dogrulama/{code}', [\App\Http\Controllers\CareerController::class, 'verifyCertificate'])->name('internship.verify');
Route::post('/stajyer/cikis', [\App\Http\Controllers\CareerController::class, 'internLogout'])->name('intern.logout');