feat: add approval system for internship journal entries including database fields, admin toggle functionality, and rich-text rendering
This commit is contained in:
@@ -199,7 +199,13 @@ class InternApplicationResource extends Resource
|
||||
|
||||
$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) {
|
||||
$dayNum = $d['day_number'];
|
||||
$dateF = $d['formatted_date'];
|
||||
@@ -220,11 +226,114 @@ class InternApplicationResource extends Resource
|
||||
$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>';
|
||||
|
||||
$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>';
|
||||
|
||||
// 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
|
||||
$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>';
|
||||
|
||||
@@ -529,5 +529,37 @@ class CareerController extends Controller
|
||||
'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.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,14 @@ class InternshipJournalEntry extends Model
|
||||
'date',
|
||||
'content',
|
||||
'is_retroactive',
|
||||
'supervisor_approved',
|
||||
'unit_approved',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_retroactive' => 'boolean',
|
||||
'supervisor_approved' => 'boolean',
|
||||
'unit_approved' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+29
@@ -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 => {
|
||||
const dateStr = item.commit.author.date;
|
||||
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]) {
|
||||
allGithubCommits[dateOnly] = [];
|
||||
}
|
||||
@@ -985,7 +992,7 @@
|
||||
const msg = c.commit.message;
|
||||
const sha = c.sha.substring(0, 7);
|
||||
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 += "</ul>";
|
||||
|
||||
@@ -420,7 +420,7 @@
|
||||
<!-- Supervisor -->
|
||||
<div class="signature-box">
|
||||
<div class="sig-title">Staj Sorumlusu</div>
|
||||
@if($intern->notebook_supervisor_signed)
|
||||
@if($entry && $entry->supervisor_approved)
|
||||
<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>
|
||||
@@ -437,7 +437,7 @@
|
||||
<!-- Unit Officer -->
|
||||
<div class="signature-box">
|
||||
<div class="sig-title">Birim Yetkilisi</div>
|
||||
@if($intern->notebook_unit_signed)
|
||||
@if($entry && $entry->unit_approved)
|
||||
<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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@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">
|
||||
|
||||
@if($application->status === 'accepted')
|
||||
@if($application->status === 'accepted' && $application->notebook_approved)
|
||||
<div class="bg-slate-100 py-12 no-print">
|
||||
<div class="container max-w-6xl mx-auto px-4">
|
||||
<!-- Verification Banner (e-Devlet Style) -->
|
||||
@@ -68,7 +68,7 @@
|
||||
<p>
|
||||
<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>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 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.
|
||||
@@ -212,7 +212,15 @@
|
||||
@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-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>
|
||||
</div>
|
||||
<div class="text-xs text-slate-600 leading-relaxed verify-log-content" style="font-size: 11px;">{!! $entry->content !!}</div>
|
||||
|
||||
@@ -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::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::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');
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user