feat: implement quick approval dashboard for intern reports with filtered views and modal navigation

This commit is contained in:
Ümit Tunç
2026-07-17 21:40:09 +03:00
parent 161d1c86bc
commit e172afb3ac
9 changed files with 1491 additions and 0 deletions
+57
View File
@@ -427,9 +427,18 @@ class CareerController extends Controller
];
});
$unapprovedCount = \App\Models\InternshipJournalEntry::where('supervisor_approved', false)
->whereNotNull('content')
->where('content', '!=', '')
->whereHas('careerApplication', function ($q) {
$q->where('type', 'internship');
})
->count();
return view('front.career.admin_dashboard', [
'allInterns' => $allInterns,
'ganttInterns' => $ganttInterns,
'unapprovedCount' => $unapprovedCount,
'meta' => [
'title' => 'Staj Yönetici Paneli',
'description' => 'Stajyer bilgilerini ve takvimlerini izleyin.',
@@ -740,5 +749,53 @@ class CareerController extends Controller
]
]);
}
public function getQuickApprovalEntries(Request $request)
{
if (!auth()->check() || !auth()->user()->hasRole('super_admin')) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$type = $request->query('type', 'unapproved'); // 'unapproved' or 'today'
$date = $request->query('date');
$query = \App\Models\InternshipJournalEntry::with('careerApplication')
->whereHas('careerApplication', function ($q) {
$q->where('type', 'internship');
});
if ($type === 'today') {
$targetDate = $date ?: \Carbon\Carbon::today()->format('Y-m-d');
$query->whereDate('date', $targetDate);
} elseif ($type === 'unapproved') {
$query->where('supervisor_approved', false);
}
// Only return entries that have content
$query->whereNotNull('content')->where('content', '!=', '');
$entries = $query->orderBy('date', 'desc')->get()->map(function ($entry) {
return [
'id' => $entry->id,
'day_number' => $entry->day_number,
'date' => $entry->date,
'formatted_date' => \Carbon\Carbon::parse($entry->date)->format('d.m.Y'),
'content' => $entry->content,
'is_retroactive' => (bool)$entry->is_retroactive,
'supervisor_approved' => (bool)$entry->supervisor_approved,
'supervisor_name' => $entry->supervisor_name,
'intern' => [
'id' => $entry->careerApplication->id,
'name' => $entry->careerApplication->name,
'email' => $entry->careerApplication->email,
]
];
});
return response()->json([
'success' => true,
'entries' => $entries
]);
}
}
@@ -23,6 +23,14 @@
<i class="uil uil-list-ul text-lg"></i>
<span>Stajyer Listesi ve Detaylı Takip</span>
</button>
<button type="button" role="tab" aria-selected="false" data-tab-target="quick-approval" class="admin-tab-btn flex-1 flex items-center justify-center gap-3 py-4 px-6 rounded-2xl text-center font-bold text-sm transition-all border border-transparent text-slate-600 hover:bg-slate-50 cursor-pointer relative">
<i class="uil uil-check-square text-lg"></i>
<span>Hızlı Defter Onaylama</span>
<span id="quick-approval-badge" class="{{ $unapprovedCount > 0 ? '' : 'hidden' }} absolute -top-1 -right-1 flex h-5 min-w-[20px] items-center justify-center rounded-full bg-rose-500 px-1.5 text-[10px] font-black text-white ring-2 ring-white shadow-md">
{{ $unapprovedCount }}
</span>
</button>
</div>
</div>
@@ -35,6 +43,10 @@
@include('front.career.partials.list')
</div>
<div id="quick-approval-panel" class="admin-tab-panel hidden space-y-6">
@include('front.career.partials.quick_approval')
</div>
</div>
</div>
</section>
@@ -632,6 +644,12 @@
}
}
}
if (targetId === 'quick-approval') {
if (typeof loadQuickApprovalEntries === 'function') {
loadQuickApprovalEntries();
}
}
}
});
});
@@ -0,0 +1,525 @@
<!-- Quick Approval Management View -->
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50">
<!-- Header & Description -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-8 pb-6 border-b border-slate-100">
<div>
<h3 class="font-bold text-slate-800 text-lg flex items-center gap-2">
<i class="uil uil-check-square text-[#e31e24] text-xl"></i>
<span>Hızlı Defter Onaylama</span>
</h3>
<p class="text-xs text-slate-400 font-semibold mt-1">Stajyerlerin günlük yazdığı raporları tek bir ekrandan inceleyin ve onaylayın.</p>
</div>
<!-- Filter Options -->
<div class="flex flex-wrap items-center gap-3">
<div class="flex bg-slate-50 p-1 rounded-xl border border-slate-200/50">
<button type="button" onclick="setQuickFilter('unapproved')" id="qf-btn-unapproved" class="px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer bg-white text-blue-600 shadow-sm border border-slate-200/20">
Onay Bekleyenler
</button>
<button type="button" onclick="setQuickFilter('today')" id="qf-btn-today" class="px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer text-slate-600 hover:text-slate-900">
Bugün Yazılanlar
</button>
</div>
<!-- Date Picker for custom date -->
<div id="qf-date-wrapper" class="hidden">
<input type="date" id="qf-date-input" onchange="loadQuickApprovalEntries()" value="{{ date('Y-m-d') }}" class="text-xs font-semibold text-slate-700 bg-white border border-slate-200 rounded-xl px-3 py-2 focus:outline-none focus:border-blue-400">
</div>
</div>
</div>
<!-- Loading State -->
<div id="qa-loading" class="flex flex-col items-center justify-center py-16">
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 mb-3"></div>
<p class="text-xs font-semibold text-slate-400">Defter kayıtları yükleniyor...</p>
</div>
<!-- Empty State -->
<div id="qa-empty" class="hidden flex flex-col items-center justify-center py-16 text-center">
<div class="w-16 h-16 bg-emerald-50 rounded-2xl text-emerald-500 flex items-center justify-center text-3xl mb-4 border border-emerald-100">
<i class="uil uil-smile-beam"></i>
</div>
<h4 id="qa-empty-title" class="font-extrabold text-slate-800 text-sm">Harika! Onay Bekleyen Defter Yok</h4>
<p id="qa-empty-desc" class="text-xs text-slate-400 font-semibold mt-1">Şu anda onayınızı bekleyen hiçbir stajyer raporu bulunmuyor.</p>
</div>
<!-- Content List Grid -->
<div id="qa-list-grid" class="hidden grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Loaded dynamically -->
</div>
</div>
<!-- Dedicated Quick Approval Entry Reading Modal -->
<div id="quick-entry-modal" class="fixed inset-0 z-[9999] hidden items-center justify-center bg-slate-900/60 backdrop-blur-sm transition-opacity duration-300">
<div class="bg-white rounded-3xl shadow-2xl border border-slate-100 max-w-2xl w-full mx-4 overflow-hidden transform scale-95 opacity-0 transition-all duration-300 flex flex-col max-h-[85vh]" id="quick-entry-modal-card">
<!-- Header -->
<div class="px-6 py-4 bg-gradient-to-r from-blue-50 to-indigo-50/30 border-b border-slate-100 flex items-center justify-between flex-shrink-0">
<div>
<h4 id="qem-intern-name" class="font-bold text-slate-800 text-sm">Stajyer İsmi</h4>
<p id="qem-meta" class="text-xs text-slate-400 font-semibold mt-0.5"></p>
</div>
<button type="button" onclick="closeQuickEntryModal()" class="text-slate-400 hover:text-slate-600 transition-colors p-1.5 rounded-full hover:bg-slate-100/80">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<!-- Body -->
<div class="flex-grow p-6 overflow-y-auto no-scrollbar flex flex-col min-h-[250px]">
<div id="qem-retroactive-badge" class="mb-4">
<!-- Retroactive indicator -->
</div>
<div id="qem-content" class="prose max-w-none text-slate-700 leading-relaxed text-sm select-text flex-grow">
<!-- Full text rendered safely -->
</div>
</div>
<!-- Footer Controls -->
<div class="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-4 flex-shrink-0">
<!-- Carousel navigation -->
<div class="flex items-center gap-2">
<button type="button" id="qem-btn-prev" onclick="prevQuickEntry()" class="px-3 py-1.5 bg-white hover:bg-slate-100 border border-slate-200 text-slate-600 rounded-xl text-xs font-bold transition-all flex items-center gap-1 cursor-pointer">
<i class="uil uil-angle-left-b text-sm"></i> Önceki
</button>
<span id="qem-indicator" class="text-xs font-extrabold text-slate-500 bg-slate-200/50 px-2.5 py-1.5 rounded-lg">0 / 0</span>
<button type="button" id="qem-btn-next" onclick="nextQuickEntry()" class="px-3 py-1.5 bg-white hover:bg-slate-100 border border-slate-200 text-slate-600 rounded-xl text-xs font-bold transition-all flex items-center gap-1 cursor-pointer">
Sonraki <i class="uil uil-angle-right-b text-sm"></i>
</button>
</div>
<!-- Action Button -->
<div class="flex items-center gap-3">
<button type="button" id="qem-action-btn" onclick="toggleQuickEntryApproval()" class="px-5 py-2.5 text-white rounded-xl font-bold text-xs shadow-lg transition-all cursor-pointer">
Onayla
</button>
<button type="button" onclick="closeQuickEntryModal()" class="px-4 py-2.5 bg-slate-800 hover:bg-slate-900 text-white font-bold rounded-xl text-xs transition-colors cursor-pointer">Kapat</button>
</div>
</div>
</div>
</div>
<!-- Safe DOM HTML Sanitizer script -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.6/purify.min.js" integrity="sha384-+VfUPEb0PdtChMwmBcBmykRMDd+v6D/oFmB3rZM/puCMDYcIvF968OimRh4KQY9a" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
let quickApprovalEntries = [];
let currentQuickFilter = 'unapproved';
let activeQuickEntryIndex = -1;
let isUpdatingQuickApproval = false;
function setQuickFilter(filter) {
currentQuickFilter = filter;
// Toggle active state classes on filter buttons
const btnUnapproved = document.getElementById('qf-btn-unapproved');
const btnToday = document.getElementById('qf-btn-today');
const dateWrapper = document.getElementById('qf-date-wrapper');
if (filter === 'unapproved') {
btnUnapproved.className = "px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer bg-white text-blue-600 shadow-sm border border-slate-200/20";
btnToday.className = "px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer text-slate-600 hover:text-slate-900";
dateWrapper.classList.add('hidden');
} else {
btnToday.className = "px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer bg-white text-blue-600 shadow-sm border border-slate-200/20";
btnUnapproved.className = "px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer text-slate-600 hover:text-slate-900";
dateWrapper.classList.remove('hidden');
}
loadQuickApprovalEntries();
}
function loadQuickApprovalEntries() {
const listGrid = document.getElementById('qa-list-grid');
const loadingEl = document.getElementById('qa-loading');
const emptyEl = document.getElementById('qa-empty');
listGrid.classList.add('hidden');
emptyEl.classList.add('hidden');
loadingEl.classList.remove('hidden');
let url = `/stajyer/admin/quick-approval-entries?type=${currentQuickFilter}`;
if (currentQuickFilter === 'today') {
const selectedDate = document.getElementById('qf-date-input').value;
if (selectedDate) {
url += `&date=${selectedDate}`;
}
}
fetch(url)
.then(res => res.json())
.then(data => {
loadingEl.classList.add('hidden');
if (!data.success) {
alert('Kayıtlar yüklenemedi.');
return;
}
quickApprovalEntries = data.entries;
// Update unapproved badge dynamic count if filter is unapproved
if (currentQuickFilter === 'unapproved') {
updateBadgeCount(quickApprovalEntries.length);
}
if (quickApprovalEntries.length === 0) {
const emptyTitle = document.getElementById('qa-empty-title');
const emptyDesc = document.getElementById('qa-empty-desc');
if (currentQuickFilter === 'unapproved') {
emptyTitle.textContent = "Onay Bekleyen Defter Yok";
emptyDesc.textContent = "Harika! Şu anda onayınızı bekleyen hiçbir stajyer raporu bulunmuyor.";
} else {
emptyTitle.textContent = "Kayıt Bulunamadı";
emptyDesc.textContent = "Seçilen tarihte yazılmış herhangi bir stajyer defteri kaydı bulunmuyor.";
}
emptyEl.classList.remove('hidden');
return;
}
renderQuickListGrid();
listGrid.classList.remove('hidden');
})
.catch(err => {
console.error(err);
loadingEl.classList.add('hidden');
alert('Sunucuyla bağlantı kurulurken bir hata oluştu.');
});
}
function renderQuickListGrid() {
const grid = document.getElementById('qa-list-grid');
grid.innerHTML = '';
quickApprovalEntries.forEach((entry, index) => {
// Helper to strip HTML tags for a clean content preview snippet
const tempDiv = document.createElement('div');
tempDiv.innerHTML = entry.content;
const cleanText = tempDiv.textContent || tempDiv.innerText || "";
const snippet = cleanText.length > 120 ? cleanText.substring(0, 120) + '...' : cleanText;
const card = document.createElement('div');
card.id = `qa-card-${entry.id}`;
card.className = "bg-slate-50/50 rounded-2xl p-5 border border-slate-200/40 hover:border-blue-200 hover:bg-blue-50/5 transition-all flex flex-col justify-between shadow-sm relative";
let badgeHtml = '';
if (entry.supervisor_approved) {
badgeHtml = `<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase bg-emerald-50 text-emerald-700 border border-emerald-100">ONAYLI</span>`;
} else {
badgeHtml = `<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase bg-amber-50 text-amber-700 border border-amber-200">ONAY BEKLİYOR</span>`;
}
let retroHtml = '';
if (entry.is_retroactive) {
retroHtml = `<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase bg-rose-50 text-rose-600 border border-rose-100">Geriye Dönük</span>`;
}
card.innerHTML = `
<div class="mb-4">
<!-- Top Row -->
<div class="flex items-start justify-between gap-2 mb-3">
<div>
<h4 class="font-extrabold text-slate-800 text-sm hover:underline hover:text-blue-600 cursor-pointer" onclick="openInternJournalModal(${entry.intern.id})">
${escapeHTML(entry.intern.name)}
</h4>
<span class="text-[10px] text-slate-400 font-semibold block mt-0.5">${escapeHTML(entry.intern.email)}</span>
</div>
<div class="flex flex-col items-end gap-1.5">
${badgeHtml}
${retroHtml}
</div>
</div>
<!-- Day & Date Info -->
<div class="mb-3 text-[11px] font-extrabold text-blue-600 bg-blue-50/50 px-2.5 py-1.5 rounded-xl w-max">
${entry.day_number}. Gün (${entry.formatted_date})
</div>
<!-- Snippet Content Preview -->
<p class="text-xs text-slate-600 leading-relaxed font-medium bg-white p-3.5 rounded-xl border border-slate-100/80 min-h-[60px] italic">
"${escapeHTML(snippet)}"
</p>
</div>
<!-- Card Actions -->
<div class="pt-3 border-t border-slate-200/50 flex items-center justify-between gap-3">
<button type="button" onclick="openQuickEntryModal(${index})" class="px-3.5 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold rounded-xl text-[11px] transition-all cursor-pointer flex items-center gap-1.5">
<i class="uil uil-book-open"></i> Oku & Onayla
</button>
${!entry.supervisor_approved ? `
<button type="button" id="quick-approve-btn-${entry.id}" onclick="quickApproveEntry(${entry.id}, ${index})" class="px-3.5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white font-bold rounded-xl text-[11px] transition-all shadow-md shadow-emerald-500/10 cursor-pointer flex items-center gap-1">
<i class="uil uil-check"></i> Hızlı Onayla
</button>
` : `
<button type="button" id="quick-approve-btn-${entry.id}" onclick="quickApproveEntry(${entry.id}, ${index})" class="px-3.5 py-2 bg-rose-50 hover:bg-rose-100 text-rose-600 border border-rose-200 font-bold rounded-xl text-[11px] transition-all cursor-pointer flex items-center gap-1">
<i class="uil uil-times"></i> Onayı Kaldır
</button>
`}
</div>
`;
grid.appendChild(card);
});
}
function updateBadgeCount(count) {
const badge = document.getElementById('quick-approval-badge');
if (badge) {
badge.textContent = count;
if (count > 0) {
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
}
function quickApproveEntry(entryId, index) {
const btn = document.getElementById(`quick-approve-btn-${entryId}`);
if (!btn || isUpdatingQuickApproval) return;
isUpdatingQuickApproval = true;
btn.style.opacity = '0.5';
fetch('/stajyer/admin/toggle-approval', {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({
entry_id: entryId
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
// Update entry status in local array
quickApprovalEntries[index].supervisor_approved = data.status;
quickApprovalEntries[index].supervisor_name = data.supervisor_name;
// If in "unapproved" view, and we approved, we can slide out the card nicely
if (currentQuickFilter === 'unapproved' && data.status) {
const card = document.getElementById(`qa-card-${entryId}`);
if (card) {
card.style.transition = 'all 0.3s ease-out';
card.style.transform = 'scale(0.95)';
card.style.opacity = '0';
setTimeout(() => {
// Remove item from array and re-render or just remove the element
quickApprovalEntries = quickApprovalEntries.filter(e => e.id !== entryId);
updateBadgeCount(quickApprovalEntries.length);
if (quickApprovalEntries.length === 0) {
document.getElementById('qa-list-grid').classList.add('hidden');
document.getElementById('qa-empty').classList.remove('hidden');
} else {
renderQuickListGrid();
}
}, 300);
}
} else {
// Just re-render list
renderQuickListGrid();
}
} else {
alert(data.message || 'Onay güncellenemedi.');
}
})
.catch(err => {
console.error(err);
alert('İşlem gerçekleştirilemedi.');
})
.finally(() => {
isUpdatingQuickApproval = false;
if (btn) btn.style.opacity = '1';
});
}
// Quick entry modal logic
function openQuickEntryModal(index) {
activeQuickEntryIndex = index;
const modal = document.getElementById('quick-entry-modal');
if (!modal) return;
modal.classList.remove('hidden');
modal.classList.add('flex');
const card = document.getElementById('quick-entry-modal-card');
if (card) {
setTimeout(() => {
card.classList.remove('scale-95', 'opacity-0');
card.classList.add('scale-100', 'opacity-100');
}, 50);
}
renderQuickEntryDetails();
}
function closeQuickEntryModal() {
const modal = document.getElementById('quick-entry-modal');
if (!modal) return;
const card = document.getElementById('quick-entry-modal-card');
if (card) {
card.classList.remove('scale-100', 'opacity-100');
card.classList.add('scale-95', 'opacity-0');
}
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
}, 200);
}
function renderQuickEntryDetails() {
if (activeQuickEntryIndex < 0 || activeQuickEntryIndex >= quickApprovalEntries.length) return;
const entry = quickApprovalEntries[activeQuickEntryIndex];
// Header details
document.getElementById('qem-intern-name').textContent = entry.intern.name;
document.getElementById('qem-meta').textContent = `${entry.day_number}. Gün - ${entry.formatted_date}`;
// Retroactive badge
const retroBadge = document.getElementById('qem-retroactive-badge');
retroBadge.innerHTML = '';
if (entry.is_retroactive) {
retroBadge.innerHTML = `
<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-rose-50 text-rose-700 text-[10px] font-extrabold uppercase border border-rose-100">
<span class="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse"></span>
Geriye Dönük Kayıt
</span>
`;
}
// Body content (Sanitized using DOMPurify!)
const cleanHTML = DOMPurify.sanitize(entry.content);
document.getElementById('qem-content').innerHTML = cleanHTML || '<p class="text-slate-400 italic">Boş içerik.</p>';
// Carousel buttons indicators
document.getElementById('qem-indicator').textContent = `${activeQuickEntryIndex + 1} / ${quickApprovalEntries.length}`;
const btnPrev = document.getElementById('qem-btn-prev');
const btnNext = document.getElementById('qem-btn-next');
btnPrev.disabled = activeQuickEntryIndex === 0;
btnPrev.style.opacity = activeQuickEntryIndex === 0 ? '0.4' : '1';
btnPrev.style.cursor = activeQuickEntryIndex === 0 ? 'not-allowed' : 'pointer';
btnNext.disabled = activeQuickEntryIndex === quickApprovalEntries.length - 1;
btnNext.style.opacity = activeQuickEntryIndex === quickApprovalEntries.length - 1 ? '0.4' : '1';
btnNext.style.cursor = activeQuickEntryIndex === quickApprovalEntries.length - 1 ? 'not-allowed' : 'pointer';
// Action button
const actBtn = document.getElementById('qem-action-btn');
if (entry.supervisor_approved) {
actBtn.textContent = "Onayı Kaldır";
actBtn.className = "px-5 py-2.5 text-white bg-rose-600 hover:bg-rose-700 rounded-xl font-bold text-xs shadow-lg shadow-rose-500/20 transition-all cursor-pointer";
} else {
actBtn.textContent = "Günü Onayla";
actBtn.className = "px-5 py-2.5 text-white bg-emerald-600 hover:bg-emerald-700 rounded-xl font-bold text-xs shadow-lg shadow-emerald-500/20 transition-all cursor-pointer";
}
}
function prevQuickEntry() {
if (activeQuickEntryIndex > 0) {
activeQuickEntryIndex--;
renderQuickEntryDetails();
}
}
function nextQuickEntry() {
if (activeQuickEntryIndex < quickApprovalEntries.length - 1) {
activeQuickEntryIndex++;
renderQuickEntryDetails();
}
}
function toggleQuickEntryApproval() {
if (activeQuickEntryIndex < 0 || isUpdatingQuickApproval) return;
const entry = quickApprovalEntries[activeQuickEntryIndex];
const actBtn = document.getElementById('qem-action-btn');
isUpdatingQuickApproval = true;
actBtn.style.opacity = '0.5';
fetch('/stajyer/admin/toggle-approval', {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({
entry_id: entry.id
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
entry.supervisor_approved = data.status;
entry.supervisor_name = data.supervisor_name;
// If in unapproved filter, and we approved, we'll remove it after closing/switching, or we can handle it seamlessly
// For modal, it's nice to either show updated status, or automatically move to next unapproved entry!
if (currentQuickFilter === 'unapproved' && data.status) {
// Approved! Remove from list, decrement badge, and move to next or close if empty
const oldIndex = activeQuickEntryIndex;
quickApprovalEntries = quickApprovalEntries.filter(e => e.id !== entry.id);
updateBadgeCount(quickApprovalEntries.length);
if (quickApprovalEntries.length === 0) {
closeQuickEntryModal();
document.getElementById('qa-list-grid').classList.add('hidden');
document.getElementById('qa-empty').classList.remove('hidden');
} else {
// Keep the index clamped within the new list bounds
activeQuickEntryIndex = Math.min(oldIndex, quickApprovalEntries.length - 1);
renderQuickEntryDetails();
renderQuickListGrid();
}
} else {
// Just update UI inside modal & grid
renderQuickEntryDetails();
renderQuickListGrid();
}
} else {
alert(data.message || 'Onay güncellenemedi.');
}
})
.catch(err => {
console.error(err);
alert('İşlem gerçekleştirilemedi.');
})
.finally(() => {
isUpdatingQuickApproval = false;
actBtn.style.opacity = '1';
});
}
function escapeHTML(str) {
if (!str) return '';
return str.replace(/[&<>'"]/g,
tag => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&#39;',
'"': '&quot;'
}[tag] || tag)
);
}
// Handle ESC key for quick approval modal
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeQuickEntryModal();
}
});
</script>
+1
View File
@@ -134,6 +134,7 @@ Route::get('/stajyer/admin/panel', [\App\Http\Controllers\CareerController::clas
Route::get('/stajyer/admin/journal-entry', [\App\Http\Controllers\CareerController::class, 'getJournalEntry'])->name('intern.admin.get-journal-entry');
Route::post('/stajyer/admin/toggle-approval', [\App\Http\Controllers\CareerController::class, 'toggleJournalApproval'])->name('intern.admin.toggle-journal-approval');
Route::get('/stajyer/admin/journal-details', [\App\Http\Controllers\CareerController::class, 'getInternJournalDetails'])->name('intern.admin.get-journal-details');
Route::get('/stajyer/admin/quick-approval-entries', [\App\Http\Controllers\CareerController::class, 'getQuickApprovalEntries'])->name('intern.admin.quick-approval-entries');
Route::post('/stajyer/admin/toggle-notebook-signature', [\App\Http\Controllers\CareerController::class, 'toggleNotebookSignature'])->name('intern.admin.toggle-notebook-signature');
Route::post('/stajyer/admin/cikis', [\App\Http\Controllers\CareerController::class, 'internAdminLogout'])->name('intern.admin.logout');
+22
View File
@@ -0,0 +1,22 @@
<?php
$json = file_get_contents(__DIR__ . '/intern_data.json');
$interns = json_decode($json, true);
$report = "";
foreach ($interns as $intern) {
$report .= "========================================\n";
$report .= "NAME: " . $intern['name'] . "\n";
$report .= "Email: " . $intern['email'] . "\n";
$report .= "GitHub Repo: " . ($intern['github_repo'] ?: 'NONE') . "\n";
$report .= "Start: " . $intern['internship_start_date'] . " | End: " . $intern['internship_end_date'] . "\n";
$report .= "Total Journal Entries: " . count($intern['journal_entries']) . "\n";
foreach ($intern['journal_entries'] as $entry) {
$report .= "--- Date: " . $entry['date'] . " | Day: " . $entry['day_number'] . " | Retroactive: " . ($entry['is_retroactive'] ? 'YES' : 'NO') . " | Approved: " . ($entry['supervisor_approved'] ? 'YES' : 'NO') . "\n";
$report .= strip_tags($entry['content']) . "\n";
}
$report .= "\n\n";
}
file_put_contents(__DIR__ . '/detailed_report.txt', $report);
echo "Written to scratch/detailed_report.txt\n";
File diff suppressed because one or more lines are too long
+39
View File
@@ -0,0 +1,39 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$interns = \App\Models\CareerApplication::where('status', 'accepted')
->with(['journalEntries' => function($q) {
$q->orderBy('date', 'asc');
}])
->get();
$data = [];
foreach ($interns as $intern) {
$entries = [];
foreach ($intern->journalEntries as $entry) {
$entries[] = [
'day_number' => $entry->day_number,
'date' => $entry->date,
'content' => $entry->content,
'is_retroactive' => $entry->is_retroactive,
'supervisor_approved' => $entry->supervisor_approved,
'unit_approved' => $entry->unit_approved,
];
}
$data[] = [
'id' => $intern->id,
'name' => $intern->name,
'email' => $intern->email,
'github_username' => $intern->github_username,
'github_repo' => $intern->github_repo,
'internship_start_date' => $intern->internship_start_date,
'internship_end_date' => $intern->internship_end_date,
'journal_entries' => $entries,
];
}
file_put_contents(__DIR__ . '/intern_data.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo "Done! Saved to scratch/intern_data.json\n";
File diff suppressed because one or more lines are too long
+39
View File
@@ -0,0 +1,39 @@
<?php
$json = file_get_contents(__DIR__ . '/intern_data.json');
$interns = json_decode($json, true);
foreach ($interns as $intern) {
echo "========================================\n";
echo "NAME: " . $intern['name'] . "\n";
echo "Email: " . $intern['email'] . "\n";
echo "GitHub Repo: " . ($intern['github_repo'] ?: 'NONE') . "\n";
echo "Start: " . $intern['internship_start_date'] . " | End: " . $intern['internship_end_date'] . "\n";
echo "Total Journal Entries: " . count($intern['journal_entries']) . "\n";
// Group journal entries by week or get recent ones
// Let's filter entries for "this week" (2026-07-10 to 2026-07-17)
$this_week_entries = [];
foreach ($intern['journal_entries'] as $entry) {
if ($entry['date'] >= '2026-07-10' && $entry['date'] <= '2026-07-17') {
$this_week_entries[] = $entry;
}
}
echo "This Week's Entries (" . count($this_week_entries) . "):\n";
foreach ($this_week_entries as $entry) {
echo " - Date: " . $entry['date'] . " | Day: " . $entry['day_number'] . " | Retroactive: " . ($entry['is_retroactive'] ? 'YES' : 'NO') . "\n";
// Show first 200 chars of content
$text = trim(strip_tags($entry['content']));
$text = str_replace(["\r", "\n", "\t"], ' ', $text);
$text = preg_replace('/\s+/', ' ', $text);
echo " Content: " . mb_substr($text, 0, 150) . "...\n";
}
// Also list all entries briefly to understand overall progress
echo "All Entries dates: ";
$dates = [];
foreach ($intern['journal_entries'] as $entry) {
$dates[] = $entry['date'] . ($entry['is_retroactive'] ? '(R)' : '');
}
echo implode(', ', $dates) . "\n";
}