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
+124 -9
View File
@@ -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,7 +534,8 @@
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;
const origCertWidth = certEl.style.width;
@@ -465,7 +545,20 @@
const origTransWidth = transEl.style.width;
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';
@@ -474,7 +567,7 @@
transEl.style.width = '794px';
transEl.style.height = '1122px';
transEl.style.borderRadius = '0';
// Render Page 1 (Landscape)
const certCanvas = await html2canvas(certEl, {
scale: 2,
@@ -483,7 +576,7 @@
logging: false
});
const certImgData = certCanvas.toDataURL('image/jpeg', 1.0);
// Initialize landscape A4 document
const pdf = new jsPDF({
orientation: 'landscape',
@@ -491,7 +584,7 @@
format: 'a4'
});
pdf.addImage(certImgData, 'JPEG', 0, 0, 297, 210);
// Render Page 2 (Portrait)
const transCanvas = await html2canvas(transEl, {
scale: 2,
@@ -500,15 +593,30 @@
logging: false
});
const transImgData = transCanvas.toDataURL('image/jpeg', 1.0);
// Calculate exact height using aspect ratio to prevent vertical squishing
const transWidth = 210;
const transHeight = (transCanvas.height * transWidth) / transCanvas.width;
// Add portrait page to PDF
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;
@@ -518,7 +626,14 @@
transEl.style.width = origTransWidth;
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');