feat: implement quick approval dashboard for intern reports with filtered views and modal navigation
This commit is contained in:
@@ -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', [
|
return view('front.career.admin_dashboard', [
|
||||||
'allInterns' => $allInterns,
|
'allInterns' => $allInterns,
|
||||||
'ganttInterns' => $ganttInterns,
|
'ganttInterns' => $ganttInterns,
|
||||||
|
'unapprovedCount' => $unapprovedCount,
|
||||||
'meta' => [
|
'meta' => [
|
||||||
'title' => 'Staj Yönetici Paneli',
|
'title' => 'Staj Yönetici Paneli',
|
||||||
'description' => 'Stajyer bilgilerini ve takvimlerini izleyin.',
|
'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>
|
<i class="uil uil-list-ul text-lg"></i>
|
||||||
<span>Stajyer Listesi ve Detaylı Takip</span>
|
<span>Stajyer Listesi ve Detaylı Takip</span>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,6 +43,10 @@
|
|||||||
@include('front.career.partials.list')
|
@include('front.career.partials.list')
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="quick-approval-panel" class="admin-tab-panel hidden space-y-6">
|
||||||
|
@include('front.career.partials.quick_approval')
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 => ({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
"'": ''',
|
||||||
|
'"': '"'
|
||||||
|
}[tag] || tag)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ESC key for quick approval modal
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeQuickEntryModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -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::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::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/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/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');
|
Route::post('/stajyer/admin/cikis', [\App\Http\Controllers\CareerController::class, 'internAdminLogout'])->name('intern.admin.logout');
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user