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
@@ -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>
@@ -619,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>
@@ -634,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 -->
@@ -662,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>
<div id="commits-error" class="hidden p-4 rounded-xl bg-red-50 border border-red-200 text-[#e31e24] text-xs font-semibold">
</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>
</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>
</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>
</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>
@else
<!-- Main Workspace Grid -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- 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>
<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>
@endforeach
</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>
<!-- 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>
</div>
</div>
</div>
</div>
</div>
@endif
</div>
</div>
@@ -729,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>
@@ -766,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;")