feat: add approval system for internship journal entries including database fields, admin toggle functionality, and rich-text rendering

This commit is contained in:
Ümit Tunç
2026-07-01 21:58:56 +03:00
parent d9c1c825d9
commit f84f78a138
8 changed files with 199 additions and 9 deletions
@@ -199,7 +199,13 @@ class InternApplicationResource extends Resource
$saved = $record->journalEntries()->get()->keyBy('day_number'); $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;">'; $html = '<style>
.rich-text-content p { margin-bottom: 8px; }
.rich-text-content ul { list-style-type: disc; padding-left: 20px; margin-bottom: 8px; }
.rich-text-content ol { list-style-type: decimal; padding-left: 20px; margin-bottom: 8px; }
.rich-text-content li { margin-bottom: 4px; }
</style>';
$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) { foreach ($days as $d) {
$dayNum = $d['day_number']; $dayNum = $d['day_number'];
$dateF = $d['formatted_date']; $dateF = $d['formatted_date'];
@@ -220,11 +226,114 @@ class InternApplicationResource extends Resource
$html .= ' <span style="margin-left: auto;">' . $dateF . '</span>'; $html .= ' <span style="margin-left: auto;">' . $dateF . '</span>';
} }
$html .= ' </div>'; $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>';
$cleanContent = $content ? strip_tags($content, ['p', 'strong', 'ul', 'li', 'em', 'br', 'b', 'i', 'ol', 'span']) : '<em style="color:#94a3b8;">Rapor yazılmamış</em>';
$html .= ' <div class="rich-text-content" style="font-size:12px; color:#1e293b; line-height:1.5;">' . $cleanContent . '</div>';
if ($entry && trim($content) !== '') {
$supApproved = $entry->supervisor_approved;
$unitApproved = $entry->unit_approved;
$html .= ' <div style="display:flex; align-items:center; gap:12px; margin-top:12px; padding-top:10px; border-top:1px dashed #e2e8f0; font-size:11px;">';
// Supervisor approval
$supBg = $supApproved ? '#d1fae5' : '#f1f5f9';
$supColor = $supApproved ? '#065f46' : '#64748b';
$supText = $supApproved ? 'Sorumlu Onayladı' : 'Sorumlu Onayı Bekliyor';
$html .= ' <span id="sup-badge-' . $entry->id . '" style="background:' . $supBg . '; color:' . $supColor . '; padding: 2px 8px; border-radius: 4px; font-weight: 700;">' . $supText . '</span>';
// Unit approval
$unitBg = $unitApproved ? '#d1fae5' : '#f1f5f9';
$unitColor = $unitApproved ? '#065f46' : '#64748b';
$unitText = $unitApproved ? 'Birim Yetkilisi Onayladı' : 'Birim Yetkilisi Onayı Bekliyor';
$html .= ' <span id="unit-badge-' . $entry->id . '" style="background:' . $unitBg . '; color:' . $unitColor . '; padding: 2px 8px; border-radius: 4px; font-weight: 700;">' . $unitText . '</span>';
// Buttons
$supBtnText = $supApproved ? 'Onayı Kaldır' : 'Onayla';
$supBtnBg = $supApproved ? '#ef4444' : '#2563eb';
$html .= ' <button type="button" onclick="toggleApproval(' . $entry->id . ', \'supervisor\', this)" style="margin-left:auto; background:' . $supBtnBg . '; color:white; border:none; padding:4px 10px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:10px;">Sorumlu ' . $supBtnText . '</button>';
$unitBtnText = $unitApproved ? 'Onayı Kaldır' : 'Onayla';
$unitBtnBg = $unitApproved ? '#ef4444' : '#2563eb';
$html .= ' <button type="button" onclick="toggleApproval(' . $entry->id . ', \'unit\', this)" style="background:' . $unitBtnBg . '; color:white; border:none; padding:4px 10px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:10px;">Yetkili ' . $unitBtnText . '</button>';
$html .= ' </div>';
}
$html .= '</div>'; $html .= '</div>';
} }
$html .= '</div>'; $html .= '</div>';
// JS handler
$html .= '
<script>
if (typeof window.toggleApproval !== "function") {
window.toggleApproval = function(entryId, type, btn) {
btn.disabled = true;
btn.style.opacity = "0.5";
fetch("' . route('intern.admin.toggle-journal-approval') . '", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "' . csrf_token() . '"
},
body: JSON.stringify({
entry_id: entryId,
type: type
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (type === "supervisor") {
const badge = document.getElementById("sup-badge-" + entryId);
if (data.status) {
badge.style.background = "#d1fae5";
badge.style.color = "#065f46";
badge.textContent = "Sorumlu Onayladı";
btn.textContent = "Sorumlu Onayı Kaldır";
btn.style.background = "#ef4444";
} else {
badge.style.background = "#f1f5f9";
badge.style.color = "#64748b";
badge.textContent = "Sorumlu Onayı Bekliyor";
btn.textContent = "Sorumlu Onayla";
btn.style.background = "#2563eb";
}
} else {
const badge = document.getElementById("unit-badge-" + entryId);
if (data.status) {
badge.style.background = "#d1fae5";
badge.style.color = "#065f46";
badge.textContent = "Birim Yetkilisi Onayladı";
btn.textContent = "Yetkili Onayı Kaldır";
btn.style.background = "#ef4444";
} else {
badge.style.background = "#f1f5f9";
badge.style.color = "#64748b";
badge.textContent = "Birim Yetkilisi Onayı Bekliyor";
btn.textContent = "Yetkili Onayla";
btn.style.background = "#2563eb";
}
}
} else {
alert(data.message || "Bir hata oluştu.");
}
})
.catch(err => {
console.error(err);
alert("Bağlantı hatası oluştu.");
})
.finally(() => {
btn.disabled = false;
btn.style.opacity = "1";
});
};
}
</script>
';
// Add preview buttons // Add preview buttons
$html .= '<div style="margin-top: 15px; display: flex; gap: 10px;">'; $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=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>';
+32
View File
@@ -529,5 +529,37 @@ class CareerController extends Controller
'size' => $size, 'size' => $size,
]); ]);
} }
public function toggleJournalApproval(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'entry_id' => 'required|integer|exists:internship_journal_entries,id',
'type' => 'required|string|in:supervisor,unit',
]);
$entry = \App\Models\InternshipJournalEntry::findOrFail($request->entry_id);
if ($request->type === 'supervisor') {
$entry->supervisor_approved = !$entry->supervisor_approved;
$entry->save();
return response()->json([
'success' => true,
'status' => $entry->supervisor_approved,
'message' => 'Staj sorumlusu onayı güncellendi.'
]);
} else {
$entry->unit_approved = !$entry->unit_approved;
$entry->save();
return response()->json([
'success' => true,
'status' => $entry->unit_approved,
'message' => 'Birim yetkilisi onayı güncellendi.'
]);
}
}
} }
+4
View File
@@ -15,10 +15,14 @@ class InternshipJournalEntry extends Model
'date', 'date',
'content', 'content',
'is_retroactive', 'is_retroactive',
'supervisor_approved',
'unit_approved',
]; ];
protected $casts = [ protected $casts = [
'is_retroactive' => 'boolean', 'is_retroactive' => 'boolean',
'supervisor_approved' => 'boolean',
'unit_approved' => 'boolean',
]; ];
/** /**
@@ -0,0 +1,29 @@
<?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('internship_journal_entries', function (Blueprint $table) {
$table->boolean('supervisor_approved')->default(false);
$table->boolean('unit_approved')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->dropColumn(['supervisor_approved', 'unit_approved']);
});
}
};
@@ -887,7 +887,14 @@
commits.forEach(item => { commits.forEach(item => {
const dateStr = item.commit.author.date; const dateStr = item.commit.author.date;
if (dateStr) { if (dateStr) {
const dateOnly = dateStr.substring(0, 10); const d = new Date(dateStr);
const formatter = new Intl.DateTimeFormat('fr-CA', {
timeZone: 'Europe/Istanbul',
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
const dateOnly = formatter.format(d);
if (!allGithubCommits[dateOnly]) { if (!allGithubCommits[dateOnly]) {
allGithubCommits[dateOnly] = []; allGithubCommits[dateOnly] = [];
} }
@@ -985,7 +992,7 @@
const msg = c.commit.message; const msg = c.commit.message;
const sha = c.sha.substring(0, 7); const sha = c.sha.substring(0, 7);
const authorDate = new Date(c.commit.author.date); const authorDate = new Date(c.commit.author.date);
const time = authorDate.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' }); const time = authorDate.toLocaleTimeString('tr-TR', { timeZone: 'Europe/Istanbul', hour: '2-digit', minute: '2-digit' });
commitHtml += `<li><strong>[${time}]</strong> (<em>${sha}</em>) ${msg}</li>`; commitHtml += `<li><strong>[${time}]</strong> (<em>${sha}</em>) ${msg}</li>`;
}); });
commitHtml += "</ul>"; commitHtml += "</ul>";
+2 -2
View File
@@ -420,7 +420,7 @@
<!-- Supervisor --> <!-- Supervisor -->
<div class="signature-box"> <div class="signature-box">
<div class="sig-title">Staj Sorumlusu</div> <div class="sig-title">Staj Sorumlusu</div>
@if($intern->notebook_supervisor_signed) @if($entry && $entry->supervisor_approved)
<div class="sig-status"> <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> <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> <span class="sig-badge">E-ONAYLI</span>
@@ -437,7 +437,7 @@
<!-- Unit Officer --> <!-- Unit Officer -->
<div class="signature-box"> <div class="signature-box">
<div class="sig-title">Birim Yetkilisi</div> <div class="sig-title">Birim Yetkilisi</div>
@if($intern->notebook_unit_signed) @if($entry && $entry->unit_approved)
<div class="sig-status"> <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> <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> <span class="sig-badge">E-ONAYLI</span>
+11 -3
View File
@@ -3,7 +3,7 @@
@section('content') @section('content')
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700;800;900&family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,500;0,700;1,400&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700;800;900&family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,500;0,700;1,400&display=swap" rel="stylesheet">
@if($application->status === 'accepted') @if($application->status === 'accepted' && $application->notebook_approved)
<div class="bg-slate-100 py-12 no-print"> <div class="bg-slate-100 py-12 no-print">
<div class="container max-w-6xl mx-auto px-4"> <div class="container max-w-6xl mx-auto px-4">
<!-- Verification Banner (e-Devlet Style) --> <!-- Verification Banner (e-Devlet Style) -->
@@ -68,7 +68,7 @@
<p> <p>
<strong>{{ \Carbon\Carbon::parse($application->internship_start_date)->format('d.m.Y') }}</strong> - <strong>{{ \Carbon\Carbon::parse($application->internship_start_date)->format('d.m.Y') }}</strong> -
<strong>{{ \Carbon\Carbon::parse($application->internship_end_date)->format('d.m.Y') }}</strong> tarihleri arasında, <strong>{{ \Carbon\Carbon::parse($application->internship_end_date)->format('d.m.Y') }}</strong> tarihleri arasında,
<strong>Trunçgil Teknoloji</strong> bünyesinde gerçekleştirdiği uzaktan staj programını başarıyla tamamlayarak bu belgeyi almaya hak kazanmıştır. <strong>Trunçgil Teknoloji</strong> bünyesinde gerçekleştirdiği staj programını başarıyla tamamlayarak bu belgeyi almaya hak kazanmıştır.
</p> </p>
<p class="text-xs md:text-sm text-slate-500 max-w-2xl mx-auto italic font-serif"> <p class="text-xs md:text-sm text-slate-500 max-w-2xl mx-auto italic font-serif">
Gösterdiği üstün gayret, sorumluluk bilinci, teknik beceriler ve ekip çalışmasına sağladığı değerli katkılardan dolayı teşekkür eder, profesyonel kariyerinde başarılar dileriz. Gösterdiği üstün gayret, sorumluluk bilinci, teknik beceriler ve ekip çalışmasına sağladığı değerli katkılardan dolayı teşekkür eder, profesyonel kariyerinde başarılar dileriz.
@@ -212,7 +212,15 @@
@if($entry && trim($entry->content)) @if($entry && trim($entry->content))
<div class="p-3 bg-slate-50 border border-slate-100 rounded-xl"> <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"> <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-xs font-extrabold text-slate-800">
{{ $day['day_number'] }}. Gün Raporu
@if($entry->supervisor_approved)
<span class="ml-2 inline-flex px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">Sorumlu Onaylı</span>
@endif
@if($entry->unit_approved)
<span class="ml-1 inline-flex px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">Yetkili Onaylı</span>
@endif
</span>
<span class="text-[10px] text-slate-400 font-bold">{{ $day['formatted_date'] }}</span> <span class="text-[10px] text-slate-400 font-bold">{{ $day['formatted_date'] }}</span>
</div> </div>
<div class="text-xs text-slate-600 leading-relaxed verify-log-content" style="font-size: 11px;">{!! $entry->content !!}</div> <div class="text-xs text-slate-600 leading-relaxed verify-log-content" style="font-size: 11px;">{!! $entry->content !!}</div>
+1
View File
@@ -131,6 +131,7 @@ Route::post('/stajyer/cikis', [\App\Http\Controllers\CareerController::class, 'i
Route::get('/stajyer/admin/giris', [\App\Http\Controllers\CareerController::class, 'internAdminLoginForm'])->name('intern.admin.login'); Route::get('/stajyer/admin/giris', [\App\Http\Controllers\CareerController::class, 'internAdminLoginForm'])->name('intern.admin.login');
Route::post('/stajyer/admin/giris', [\App\Http\Controllers\CareerController::class, 'internAdminLogin'])->name('intern.admin.login.submit'); Route::post('/stajyer/admin/giris', [\App\Http\Controllers\CareerController::class, 'internAdminLogin'])->name('intern.admin.login.submit');
Route::get('/stajyer/admin/panel', [\App\Http\Controllers\CareerController::class, 'internAdminDashboard'])->name('intern.admin.dashboard'); Route::get('/stajyer/admin/panel', [\App\Http\Controllers\CareerController::class, 'internAdminDashboard'])->name('intern.admin.dashboard');
Route::post('/stajyer/admin/toggle-approval', [\App\Http\Controllers\CareerController::class, 'toggleJournalApproval'])->name('intern.admin.toggle-journal-approval');
Route::post('/stajyer/admin/cikis', [\App\Http\Controllers\CareerController::class, 'internAdminLogout'])->name('intern.admin.logout'); Route::post('/stajyer/admin/cikis', [\App\Http\Controllers\CareerController::class, 'internAdminLogout'])->name('intern.admin.logout');