refactor: simplify internship approval process by removing unit approval and adding supervisor name tracking

This commit is contained in:
Ümit Tunç
2026-07-01 22:37:08 +03:00
parent f84f78a138
commit a6c250c13e
8 changed files with 337 additions and 96 deletions
@@ -232,30 +232,20 @@ class InternApplicationResource extends Resource
if ($entry && trim($content) !== '') {
$supApproved = $entry->supervisor_approved;
$unitApproved = $entry->unit_approved;
$supName = $entry->supervisor_name;
$html .= ' <div style="display:flex; align-items:center; gap:12px; margin-top:12px; padding-top:10px; border-top:1px dashed #e2e8f0; font-size:11px;">';
// Supervisor approval
$supBg = $supApproved ? '#d1fae5' : '#f1f5f9';
$supColor = $supApproved ? '#065f46' : '#64748b';
$supText = $supApproved ? 'Sorumlu Onayladı' : 'Sorumlu Onayı Bekliyor';
$supText = $supApproved ? 'Sorumlu Onayladı' . ($supName ? ' (Onaylayan: ' . e($supName) . ')' : '') : 'Sorumlu Onayı Bekliyor';
$html .= ' <span id="sup-badge-' . $entry->id . '" style="background:' . $supBg . '; color:' . $supColor . '; padding: 2px 8px; border-radius: 4px; font-weight: 700;">' . $supText . '</span>';
// Unit approval
$unitBg = $unitApproved ? '#d1fae5' : '#f1f5f9';
$unitColor = $unitApproved ? '#065f46' : '#64748b';
$unitText = $unitApproved ? 'Birim Yetkilisi Onayladı' : 'Birim Yetkilisi Onayı Bekliyor';
$html .= ' <span id="unit-badge-' . $entry->id . '" style="background:' . $unitBg . '; color:' . $unitColor . '; padding: 2px 8px; border-radius: 4px; font-weight: 700;">' . $unitText . '</span>';
// Buttons
$supBtnText = $supApproved ? 'Onayı Kaldır' : 'Onayla';
$supBtnBg = $supApproved ? '#ef4444' : '#2563eb';
$html .= ' <button type="button" onclick="toggleApproval(' . $entry->id . ', \'supervisor\', this)" style="margin-left:auto; background:' . $supBtnBg . '; color:white; border:none; padding:4px 10px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:10px;">Sorumlu ' . $supBtnText . '</button>';
$unitBtnText = $unitApproved ? 'Onayı Kaldır' : 'Onayla';
$unitBtnBg = $unitApproved ? '#ef4444' : '#2563eb';
$html .= ' <button type="button" onclick="toggleApproval(' . $entry->id . ', \'unit\', this)" style="background:' . $unitBtnBg . '; color:white; border:none; padding:4px 10px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:10px;">Yetkili ' . $unitBtnText . '</button>';
$html .= ' <button type="button" onclick="toggleApproval(' . $entry->id . ', this)" style="margin-left:auto; background:' . $supBtnBg . '; color:white; border:none; padding:4px 10px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:10px;">Sorumlu ' . $supBtnText . '</button>';
$html .= ' </div>';
}
@@ -268,7 +258,7 @@ class InternApplicationResource extends Resource
$html .= '
<script>
if (typeof window.toggleApproval !== "function") {
window.toggleApproval = function(entryId, type, btn) {
window.toggleApproval = function(entryId, btn) {
btn.disabled = true;
btn.style.opacity = "0.5";
@@ -279,19 +269,17 @@ class InternApplicationResource extends Resource
"X-CSRF-TOKEN": "' . csrf_token() . '"
},
body: JSON.stringify({
entry_id: entryId,
type: type
entry_id: entryId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (type === "supervisor") {
const badge = document.getElementById("sup-badge-" + entryId);
if (data.status) {
badge.style.background = "#d1fae5";
badge.style.color = "#065f46";
badge.textContent = "Sorumlu Onayladı";
badge.textContent = "Sorumlu Onayladı (Onaylayan: " + data.supervisor_name + ")";
btn.textContent = "Sorumlu Onayı Kaldır";
btn.style.background = "#ef4444";
} else {
@@ -301,22 +289,6 @@ class InternApplicationResource extends Resource
btn.textContent = "Sorumlu Onayla";
btn.style.background = "#2563eb";
}
} else {
const badge = document.getElementById("unit-badge-" + entryId);
if (data.status) {
badge.style.background = "#d1fae5";
badge.style.color = "#065f46";
badge.textContent = "Birim Yetkilisi Onayladı";
btn.textContent = "Yetkili Onayı Kaldır";
btn.style.background = "#ef4444";
} else {
badge.style.background = "#f1f5f9";
badge.style.color = "#64748b";
badge.textContent = "Birim Yetkilisi Onayı Bekliyor";
btn.textContent = "Yetkili Onayla";
btn.style.background = "#2563eb";
}
}
} else {
alert(data.message || "Bir hata oluştu.");
}
@@ -353,13 +325,6 @@ class InternApplicationResource extends Resource
->label('Staj Sorumlusu Adı / Ünvanı')
->placeholder('Örn: Alperen Trunç')
->default('Alperen Trunç'),
\Filament\Forms\Components\Toggle::make('notebook_unit_signed')
->label('İlgili Birim Yetkilisi İmzala / Onayla')
->live(),
TextInput::make('notebook_unit_name')
->label('Birim Yetkilisi Adı / Ünvanı')
->placeholder('Örn: Yetkili Birim Amiri')
->default('Yetkili Birim Amiri'),
\Filament\Forms\Components\Toggle::make('notebook_approved')
->label('Staj Defterini Genel Olarak Onayla')
->columnSpanFull(),
+56 -11
View File
@@ -530,6 +530,55 @@ class CareerController extends Controller
]);
}
public function getJournalEntry(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'intern_id' => 'required|integer|exists:career_applications,id',
'date' => 'required|date',
]);
$intern = \App\Models\CareerApplication::findOrFail($request->intern_id);
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
$dayNum = null;
$dateFormatted = null;
foreach ($days as $d) {
if ($d['date'] === $request->date) {
$dayNum = $d['day_number'];
$dateFormatted = $d['formatted_date'];
break;
}
}
if (!$dayNum) {
return response()->json([
'success' => false,
'message' => 'Seçilen tarih staj dönemi dışındadır veya haftasonudur.'
], 422);
}
$entry = $intern->journalEntries()->where('date', $request->date)->first();
return response()->json([
'success' => true,
'intern_name' => $intern->name,
'day_number' => $dayNum,
'date' => $request->date,
'date_formatted' => $dateFormatted,
'entry' => $entry ? [
'id' => $entry->id,
'content' => $entry->content,
'is_retroactive' => $entry->is_retroactive,
'supervisor_approved' => $entry->supervisor_approved,
'supervisor_name' => $entry->supervisor_name,
] : null
]);
}
public function toggleJournalApproval(Request $request)
{
if (!auth()->check()) {
@@ -538,28 +587,24 @@ class CareerController extends Controller
$request->validate([
'entry_id' => 'required|integer|exists:internship_journal_entries,id',
'type' => 'required|string|in:supervisor,unit',
]);
$entry = \App\Models\InternshipJournalEntry::findOrFail($request->entry_id);
if ($request->type === 'supervisor') {
$entry->supervisor_approved = !$entry->supervisor_approved;
if ($entry->supervisor_approved) {
$entry->supervisor_name = auth()->user()->name;
} else {
$entry->supervisor_name = null;
}
$entry->save();
return response()->json([
'success' => true,
'status' => $entry->supervisor_approved,
'supervisor_name' => $entry->supervisor_name,
'message' => 'Staj sorumlusu onayı güncellendi.'
]);
} else {
$entry->unit_approved = !$entry->unit_approved;
$entry->save();
return response()->json([
'success' => true,
'status' => $entry->unit_approved,
'message' => 'Birim yetkilisi onayı güncellendi.'
]);
}
}
}
+1
View File
@@ -17,6 +17,7 @@ class InternshipJournalEntry extends Model
'is_retroactive',
'supervisor_approved',
'unit_approved',
'supervisor_name',
];
protected $casts = [
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->string('supervisor_name')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->dropColumn('supervisor_name');
});
}
};
@@ -21,6 +21,40 @@
@endif
</div>
<!-- Dynamic Journal Modal -->
<div id="journal-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-[90vh]">
<!-- Modal 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">
<div>
<h4 id="modal-title" class="font-bold text-slate-800 text-lg">Staj Raporu Detayı</h4>
<p id="modal-subtitle" class="text-xs text-slate-500 font-semibold mt-0.5"></p>
</div>
<button type="button" onclick="closeJournalModal()" 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>
<!-- Modal Body -->
<div class="p-6 overflow-y-auto flex-grow prose max-w-none text-slate-700 leading-relaxed text-sm no-scrollbar">
<div id="modal-content" class="min-h-[100px] flex flex-col justify-center">
<!-- Content gets loaded here -->
</div>
</div>
<!-- Modal Footer -->
<div class="px-6 py-4 bg-slate-50/50 border-t border-slate-100 flex items-center justify-between">
<div id="modal-status-badge">
<!-- Status badge -->
</div>
<div class="flex gap-3">
<button type="button" onclick="closeJournalModal()" class="px-4 py-2 border border-slate-200 text-slate-600 hover:bg-slate-50 rounded-xl font-bold text-xs transition-colors">Kapat</button>
<button type="button" id="modal-action-btn" onclick="handleModalAction()" class="hidden px-4 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-xl font-bold text-xs shadow-lg shadow-blue-500/20 transition-all"></button>
</div>
</div>
</div>
</div>
@push('styles')
<!-- DevExtreme Gantt CSS Dependencies -->
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.5/css/dx.fluent.blue.light.css">
@@ -31,6 +65,12 @@
border-left: 2px dashed #ef4444 !important;
width: 2px !important;
}
.rich-text-content p { margin-bottom: 8px; }
.rich-text-content ul { list-style-type: disc !important; padding-left: 20px !important; margin-bottom: 8px !important; }
.rich-text-content ol { list-style-type: decimal !important; padding-left: 20px !important; margin-bottom: 8px !important; }
.rich-text-content li { margin-bottom: 4px !important; }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
<script>
tailwind = {
@@ -62,6 +102,8 @@
progress: 0
}));
let dayColumnsMap = {};
$('#gantt').dxGantt({
tasks: {
dataSource: formattedData,
@@ -105,8 +147,192 @@
start: new Date(),
cssClass: 'current-time-line'
}],
onScaleCellPrepared: function(e) {
if (e.scaleType === 'days') {
const $el = $(e.scaleElement || e.element);
const left = $el.position().left;
const width = $el.outerWidth();
const dateStr = e.startDate.toISOString().substring(0, 10);
dayColumnsMap[dateStr] = {
left: left,
right: left + width,
date: e.startDate,
dateStr: dateStr
};
}
}
});
// Grid task area cell click listener
$(document).on('click', '#gantt', function(e) {
const $target = $(e.target);
if ($target.closest('.dx-gantt-task-area').length > 0 || $target.closest('.dx-gantt-ts-area').length > 0) {
const ganttInstance = $('#gantt').dxGantt('instance');
const selectedKey = ganttInstance.option("selectedRowKey");
if (!selectedKey) {
alert("Lütfen önce sol taraftan stajyeri seçin, ardından tıklamak istediğiniz güne tıklayın.");
return;
}
const $taskArea = $('.dx-gantt-task-area').first();
if (!$taskArea.length) return;
const scrollLeft = ganttInstance._ganttView._taskAreaContainer.scrollLeft;
const clickX = e.pageX - $taskArea.offset().left;
const totalX = scrollLeft + clickX;
const cols = Object.values(dayColumnsMap);
const clickedCol = cols.find(col => totalX >= col.left && totalX <= col.right);
if (clickedCol) {
showDailyJournalPopup(selectedKey, clickedCol.dateStr);
}
}
});
});
let activeEntryId = null;
function showDailyJournalPopup(internId, dateStr) {
const modal = document.getElementById('journal-modal');
const contentDiv = document.getElementById('modal-content');
const titleH = document.getElementById('modal-title');
const subtitleP = document.getElementById('modal-subtitle');
const badgeDiv = document.getElementById('modal-status-badge');
const actionBtn = document.getElementById('modal-action-btn');
modal.classList.remove('hidden');
modal.classList.add('flex');
setTimeout(() => {
modal.firstElementChild.classList.remove('scale-95', 'opacity-0');
modal.firstElementChild.classList.add('scale-100', 'opacity-100');
}, 50);
contentDiv.innerHTML = `
<div class="flex items-center justify-center p-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
`;
titleH.textContent = "Yükleniyor...";
subtitleP.textContent = "";
badgeDiv.innerHTML = "";
actionBtn.classList.add('hidden');
activeEntryId = null;
fetch(`/stajyer/admin/journal-entry?intern_id=${internId}&date=${dateStr}`)
.then(res => res.json())
.then(data => {
if (!data.success) {
contentDiv.innerHTML = `<p class="text-slate-500 italic text-center p-8">${data.message}</p>`;
titleH.textContent = "Bilgi";
return;
}
titleH.textContent = `${data.intern_name}`;
subtitleP.textContent = `${data.day_number}. Gün Raporu — ${data.date_formatted}`;
if (!data.entry) {
contentDiv.innerHTML = `
<div class="flex flex-col items-center justify-center p-8 text-center bg-slate-50 rounded-2xl border border-dashed border-slate-200">
<i class="uil uil-file-slash text-3xl text-slate-400 mb-2"></i>
<p class="text-sm font-medium text-slate-500">Bu gün için henüz staj raporu yazılmamıştır.</p>
</div>
`;
return;
}
activeEntryId = data.entry.id;
const entryContent = data.entry.content || '<p class="text-slate-400 italic">Boş içerik.</p>';
let contentHtml = '';
if (data.entry.is_retroactive) {
contentHtml += `
<div class="mb-4 inline-flex items-center gap-1.5 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
</div>
`;
}
contentHtml += `<div class="rich-text-content prose max-w-none text-slate-700 leading-relaxed text-sm select-text">${entryContent}</div>`;
contentDiv.innerHTML = contentHtml;
updateModalStatus(data.entry.supervisor_approved, data.entry.supervisor_name);
})
.catch(err => {
console.error(err);
contentDiv.innerHTML = `<p class="text-red-500 text-center p-8 font-semibold">Veriler yüklenirken bir hata oluştu.</p>`;
titleH.textContent = "Hata";
});
}
function updateModalStatus(approved, name) {
const badgeDiv = document.getElementById('modal-status-badge');
const actionBtn = document.getElementById('modal-action-btn');
actionBtn.classList.remove('hidden');
if (approved) {
badgeDiv.innerHTML = `
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-50 text-emerald-700 text-xs font-bold border border-emerald-100">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
Sorumlu Onayladı ${name ? `(${name})` : ''}
</span>
`;
actionBtn.textContent = "Onayı Kaldır";
actionBtn.className = "px-4 py-2 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 {
badgeDiv.innerHTML = `
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-slate-100 text-slate-600 text-xs font-bold border border-slate-200">
Onay Bekliyor
</span>
`;
actionBtn.textContent = "Raporu Onayla";
actionBtn.className = "px-4 py-2 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 handleModalAction() {
if (!activeEntryId) return;
const actionBtn = document.getElementById('modal-action-btn');
actionBtn.disabled = true;
actionBtn.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: activeEntryId
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
updateModalStatus(data.status, data.supervisor_name);
} else {
alert(data.message || "Onay güncellenemedi.");
}
})
.catch(err => {
console.error(err);
alert("İşlem sırasında bir hata oluştu.");
})
.finally(() => {
actionBtn.disabled = false;
actionBtn.style.opacity = "1";
});
}
function closeJournalModal() {
const modal = document.getElementById('journal-modal');
modal.firstElementChild.classList.add('scale-95', 'opacity-0');
modal.firstElementChild.classList.remove('scale-100', 'opacity-100');
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
}, 200);
}
</script>
@endif
@endpush
+1 -18
View File
@@ -425,24 +425,7 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color: #059669;"><polyline points="20 6 9 17 4 12"/></svg>
<span class="sig-badge">E-ONAYLI</span>
</div>
<div class="sig-name">{{ $intern->notebook_supervisor_name ?: 'Alperen Trunç' }}</div>
@else
<div class="sig-status waiting">
<span class="sig-badge waiting">İMZA BEKLENİYOR</span>
</div>
<div class="sig-name">&nbsp;</div>
@endif
</div>
<!-- Unit Officer -->
<div class="signature-box">
<div class="sig-title">Birim Yetkilisi</div>
@if($entry && $entry->unit_approved)
<div class="sig-status">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color: #059669;"><polyline points="20 6 9 17 4 12"/></svg>
<span class="sig-badge">E-ONAYLI</span>
</div>
<div class="sig-name">{{ $intern->notebook_unit_name ?: 'Yetkili Birim Amiri' }}</div>
<div class="sig-name">{{ $entry->supervisor_name ?: ($intern->notebook_supervisor_name ?: 'Alperen Trunç') }}</div>
@else
<div class="sig-status waiting">
<span class="sig-badge waiting">İMZA BEKLENİYOR</span>
+2 -10
View File
@@ -165,7 +165,7 @@
</div>
<!-- ================= PAGE 3: STAJ DEFTERİ (PORTRAIT A4) ================= -->
@if($application->notebook_approved || $application->notebook_supervisor_signed || $application->notebook_unit_signed)
@if($application->notebook_approved || $application->notebook_supervisor_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">
@@ -195,11 +195,6 @@
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>
@@ -215,10 +210,7 @@
<span class="text-xs font-extrabold text-slate-800">
{{ $day['day_number'] }}. Gün Raporu
@if($entry->supervisor_approved)
<span class="ml-2 inline-flex px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">Sorumlu Onaylı</span>
@endif
@if($entry->unit_approved)
<span class="ml-1 inline-flex px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">Yetkili Onaylı</span>
<span class="ml-2 inline-flex px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">Sorumlu Onaylı{{ $entry->supervisor_name ? ' (' . $entry->supervisor_name . ')' : '' }}</span>
@endif
</span>
<span class="text-[10px] text-slate-400 font-bold">{{ $day['formatted_date'] }}</span>
+1
View File
@@ -131,6 +131,7 @@ Route::post('/stajyer/cikis', [\App\Http\Controllers\CareerController::class, 'i
Route::get('/stajyer/admin/giris', [\App\Http\Controllers\CareerController::class, 'internAdminLoginForm'])->name('intern.admin.login');
Route::post('/stajyer/admin/giris', [\App\Http\Controllers\CareerController::class, 'internAdminLogin'])->name('intern.admin.login.submit');
Route::get('/stajyer/admin/panel', [\App\Http\Controllers\CareerController::class, 'internAdminDashboard'])->name('intern.admin.dashboard');
Route::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/cikis', [\App\Http\Controllers\CareerController::class, 'internAdminLogout'])->name('intern.admin.logout');