feat: integrate Quill editor for internship journal entries, add retroactive field support, and enforce future date restrictions

This commit is contained in:
Ümit Tunç
2026-07-01 17:43:22 +03:00
parent edc843fa1c
commit 351b9064af
7 changed files with 197 additions and 22 deletions
@@ -203,12 +203,22 @@ class InternApplicationResource extends Resource
foreach ($days as $d) { foreach ($days as $d) {
$dayNum = $d['day_number']; $dayNum = $d['day_number'];
$dateF = $d['formatted_date']; $dateF = $d['formatted_date'];
$content = isset($saved[$dayNum]) ? $saved[$dayNum]->content : ''; $entry = $saved->get($dayNum);
$content = $entry ? $entry->content : '';
$isRetro = $entry ? $entry->is_retroactive : false;
$updatedAt = $entry ? $entry->updated_at->format('d.m.Y H:i') : null;
$html .= '<div style="margin-bottom: 12px; padding: 12px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc;">'; $html .= '<div style="margin-bottom: 12px; padding: 12px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc;">';
$html .= ' <div style="display:flex; justify-content:between; font-size:11px; font-weight:700; color:#475569; border-bottom:1px solid #e2e8f0; padding-bottom:6px; margin-bottom:8px;">'; $html .= ' <div style="display:flex; justify-content:between; font-size:11px; font-weight:700; color:#475569; border-bottom:1px solid #e2e8f0; padding-bottom:6px; margin-bottom:8px;">';
$html .= ' <span style="font-weight: 800; color: #2563eb;">' . $dayNum . '. Gün Raporu</span>'; $html .= ' <span style="font-weight: 800; color: #2563eb;">' . $dayNum . '. Gün Raporu</span>';
$html .= ' <span style="margin-left: auto;">' . $dateF . '</span>'; if ($isRetro) {
$html .= ' <span style="margin-left: 10px; background: #fee2e2; color: #991b1b; padding: 1px 6px; border-radius: 4px; font-size: 9px; font-weight: 800;">GERİYE DÖNÜK KAYIT</span>';
}
if ($updatedAt) {
$html .= ' <span style="margin-left: auto;">Son Güncelleme: ' . $updatedAt . '</span>';
} else {
$html .= ' <span style="margin-left: auto;">' . $dateF . '</span>';
}
$html .= ' </div>'; $html .= ' </div>';
$html .= ' <div style="font-size:12px; color:#1e293b; white-space:pre-wrap; line-height:1.5;">' . ($content ? e($content) : '<em style="color:#94a3b8;">Rapor yazılmamış</em>') . '</div>'; $html .= ' <div style="font-size:12px; color:#1e293b; white-space:pre-wrap; line-height:1.5;">' . ($content ? e($content) : '<em style="color:#94a3b8;">Rapor yazılmamış</em>') . '</div>';
$html .= '</div>'; $html .= '</div>';
+12 -1
View File
@@ -472,21 +472,32 @@ class CareerController extends Controller
]); ]);
$dateObj = \Carbon\Carbon::parse($request->date); $dateObj = \Carbon\Carbon::parse($request->date);
// 1. Prevent future entries
if ($dateObj->isFuture()) {
return response()->json(['success' => false, 'message' => 'İleriye dönük staj günleri için defter doldurulamaz.'], 422);
}
// 2. Prevent weekend entries
if ($dateObj->isWeekend()) { if ($dateObj->isWeekend()) {
return response()->json(['success' => false, 'message' => 'Hafta sonu günlerine staj günlüğü girilemez.'], 422); return response()->json(['success' => false, 'message' => 'Hafta sonu günlerine staj günlüğü girilemez.'], 422);
} }
// 3. Determine if retroactive
$isRetroactive = $dateObj->lt(\Carbon\Carbon::today());
$entry = \App\Models\InternshipJournalEntry::updateOrCreate([ $entry = \App\Models\InternshipJournalEntry::updateOrCreate([
'career_application_id' => $intern->id, 'career_application_id' => $intern->id,
'day_number' => $request->day_number, 'day_number' => $request->day_number,
'date' => $request->date, 'date' => $request->date,
], [ ], [
'content' => $request->content, 'content' => $request->content,
'is_retroactive' => $isRetroactive,
]); ]);
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => $request->day_number . '. Gün kaydı başarıyla kaydedildi.', 'message' => $request->day_number . '. Gün kaydı başarıyla kaydedildi.' . ($isRetroactive ? ' (Geriye Dönük Kayıt)' : ''),
'entry' => $entry 'entry' => $entry
]); ]);
} }
+5
View File
@@ -14,6 +14,11 @@ class InternshipJournalEntry extends Model
'day_number', 'day_number',
'date', 'date',
'content', 'content',
'is_retroactive',
];
protected $casts = [
'is_retroactive' => 'boolean',
]; ];
/** /**
@@ -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->boolean('is_retroactive')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->dropColumn('is_retroactive');
});
}
};
@@ -722,14 +722,20 @@
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<div> <div id="future-warning" class="hidden p-4 bg-amber-50/50 border border-amber-200 rounded-2xl text-xs text-amber-700 font-semibold leading-relaxed">
<i class="uil uil-exclamation-triangle text-sm mr-1"></i> Bu staj günü henüz gerçekleşmediği için staj günlüğü yazılamaz veya düzenlenemez.
</div>
<div id="editor-wrapper">
<label class="block text-xs font-extrabold text-slate-500 uppercase tracking-wider mb-2">Günlük Çalışma Raporu</label> <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 id="quill-editor-container" class="rounded-xl border border-slate-200 overflow-hidden">
<div id="editor-content-quill" style="height: 250px;"></div>
</div>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div id="save-status" class="text-xs font-semibold text-slate-400"></div> <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"> <button type="button" id="save-btn" 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 Deftere Kaydet
</button> </button>
</div> </div>
@@ -789,6 +795,8 @@
</div> </div>
@push('styles') @push('styles')
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"></script>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
// Tab Switch Logic // Tab Switch Logic
@@ -828,6 +836,57 @@
}); });
}); });
let quill;
// Tab Switch Logic
document.addEventListener('DOMContentLoaded', function() {
// Initialize Quill editor
quill = new Quill('#editor-content-quill', {
theme: 'snow',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['clean'] // remove formatting button
]
}
});
const tabButtons = document.querySelectorAll('.tab-btn');
const tabPanels = document.querySelectorAll('.tab-panel');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
// Deactivate active tab
tabButtons.forEach(btn => {
btn.classList.remove('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
btn.classList.add('bg-transparent', 'border-transparent', 'text-slate-600');
btn.querySelector('.tab-icon').classList.remove('bg-blue-600', 'text-white');
btn.querySelector('.tab-icon').classList.add('bg-slate-50', 'text-slate-500');
btn.setAttribute('aria-selected', 'false');
});
// Hide active panels
tabPanels.forEach(panel => {
panel.classList.add('hidden');
});
// Activate clicked tab
button.classList.add('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
button.classList.remove('bg-transparent', 'border-transparent', 'text-slate-600');
button.querySelector('.tab-icon').classList.add('bg-blue-600', 'text-white');
button.querySelector('.tab-icon').classList.remove('bg-slate-50', 'text-slate-500');
button.setAttribute('aria-selected', 'true');
// Show targets panel
const targetId = button.getAttribute('data-tab-target');
const targetPanel = document.getElementById(targetId + '-panel');
if (targetPanel) {
targetPanel.classList.remove('hidden');
}
});
});
// Handle initial state of date logic // Handle initial state of date logic
calculateEndDateFrontend(); calculateEndDateFrontend();
@@ -906,8 +965,40 @@
document.getElementById('editor-day-title').textContent = `${dayNum}. Gün`; document.getElementById('editor-day-title').textContent = `${dayNum}. Gün`;
document.getElementById('editor-day-date').textContent = dateFormatted; document.getElementById('editor-day-date').textContent = dateFormatted;
document.getElementById('editor-content').value = savedContent;
quill.root.innerHTML = savedContent || '';
document.getElementById('save-status').textContent = ''; document.getElementById('save-status').textContent = '';
// Future validation
const dateParts = dateVal.split('-');
const selectedDate = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
const today = new Date();
today.setHours(0,0,0,0);
const isFuture = selectedDate > today;
const warningBlock = document.getElementById('future-warning');
const saveBtn = document.getElementById('save-btn');
const gitBtn = document.querySelector('button[onclick="fillFromGithub()"]');
if (isFuture) {
warningBlock.classList.remove('hidden');
quill.enable(false);
saveBtn.disabled = true;
saveBtn.classList.add('opacity-50', 'cursor-not-allowed');
if (gitBtn) {
gitBtn.disabled = true;
gitBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
} else {
warningBlock.classList.add('hidden');
quill.enable(true);
saveBtn.disabled = false;
saveBtn.classList.remove('opacity-50', 'cursor-not-allowed');
if (gitBtn) {
gitBtn.disabled = false;
gitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
} }
} }
@@ -923,25 +1014,26 @@
return; return;
} }
let commitText = ""; let commitHtml = "<ul>";
const sorted = [...dayCommits].reverse(); const sorted = [...dayCommits].reverse();
sorted.forEach(c => { sorted.forEach(c => {
const msg = c.commit.message; const msg = c.commit.message;
const sha = c.sha.substring(0, 7); const sha = c.sha.substring(0, 7);
const authorDate = new Date(c.commit.author.date); const authorDate = new Date(c.commit.author.date);
const time = authorDate.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' }); const time = authorDate.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
commitText += `- [${time}] (${sha}) ${msg}\n`; commitHtml += `<li><strong>[${time}]</strong> (<em>${sha}</em>) ${msg}</li>`;
}); });
commitHtml += "</ul>";
const textarea = document.getElementById('editor-content'); const currentHtml = quill.root.innerHTML.trim();
if (textarea.value.trim() !== "") { if (currentHtml !== "" && currentHtml !== "<p><br></p>") {
if (confirm("Mevcut rapor içeriğinizin sonuna bu günün commitlerini eklemek ister misiniz? (Hayır derseniz mevcut içerik silinip commitler yazılır.)")) { if (confirm("Mevcut rapor içeriğinizin sonuna bu günün commitlerini eklemek ister misiniz? (Hayır derseniz mevcut içerik silinip commitler yazılır.)")) {
textarea.value = textarea.value.trim() + "\n\n" + commitText.trim(); quill.root.innerHTML = currentHtml + "<br><br>" + commitHtml;
} else { } else {
textarea.value = commitText.trim(); quill.root.innerHTML = commitHtml;
} }
} else { } else {
textarea.value = commitText.trim(); quill.root.innerHTML = commitHtml;
} }
} }
@@ -951,7 +1043,10 @@
const dayNum = btn.getAttribute('data-day-num'); const dayNum = btn.getAttribute('data-day-num');
const dateVal = btn.getAttribute('data-date'); const dateVal = btn.getAttribute('data-date');
const contentVal = document.getElementById('editor-content').value; let contentVal = quill.root.innerHTML;
if (contentVal === '<p><br></p>') {
contentVal = '';
}
const statusText = document.getElementById('save-status'); const statusText = document.getElementById('save-status');
statusText.textContent = "Kaydediliyor..."; statusText.textContent = "Kaydediliyor...";
+16 -2
View File
@@ -207,11 +207,25 @@
background: #ffffff; background: #ffffff;
margin-bottom: 20px; margin-bottom: 20px;
min-height: 250px; min-height: 250px;
white-space: pre-wrap;
line-height: 1.6; line-height: 1.6;
color: #334155; color: #334155;
font-size: 13px; font-size: 13px;
} }
.content-area ul {
list-style-type: disc !important;
padding-left: 20px !important;
margin-top: 8px !important;
margin-bottom: 8px !important;
}
.content-area ol {
list-style-type: decimal !important;
padding-left: 20px !important;
margin-top: 8px !important;
margin-bottom: 8px !important;
}
.content-area li {
margin-bottom: 4px !important;
}
.empty-content { .empty-content {
color: #94a3b8; color: #94a3b8;
@@ -383,7 +397,7 @@
<!-- Content Area --> <!-- Content Area -->
<div class="content-area"> <div class="content-area">
@if($entry && trim($entry->content)) @if($entry && trim($entry->content))
{!! nl2br(e($entry->content)) !!} {!! $entry->content !!}
@else @else
<div class="empty-content">Bu gün için herhangi bir staj raporu girilmemiştir.</div> <div class="empty-content">Bu gün için herhangi bir staj raporu girilmemiştir.</div>
@endif @endif
+17 -5
View File
@@ -22,11 +22,8 @@
<button onclick="downloadPDF()" id="pdf-btn" class="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white text-sm font-bold rounded-lg transition-all shadow-sm flex items-center gap-1.5 whitespace-nowrap"> <button onclick="downloadPDF()" id="pdf-btn" class="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white text-sm font-bold rounded-lg transition-all shadow-sm flex items-center gap-1.5 whitespace-nowrap">
<i class="uil uil-file-download text-base" id="pdf-icon"></i> <i class="uil uil-file-download text-base" id="pdf-icon"></i>
<span class="spinner-border spinner-border-sm hidden animate-spin w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full" id="pdf-spinner" role="status"></span> <span class="spinner-border spinner-border-sm hidden animate-spin w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full" id="pdf-spinner" role="status"></span>
<span id="pdf-btn-text">PDF İndir</span>
</button>
</div> </div>
</div> </div>
@endif
</div> </div>
</div> </div>
@@ -218,7 +215,7 @@
<span class="text-xs font-extrabold text-slate-800">{{ $day['day_number'] }}. Gün Raporu</span> <span class="text-xs font-extrabold text-slate-800">{{ $day['day_number'] }}. Gün Raporu</span>
<span class="text-[10px] text-slate-400 font-bold">{{ $day['formatted_date'] }}</span> <span class="text-[10px] text-slate-400 font-bold">{{ $day['formatted_date'] }}</span>
</div> </div>
<p class="text-xs text-slate-600 whitespace-pre-wrap leading-relaxed" style="font-size: 11px;">{{ $entry->content }}</p> <div class="text-xs text-slate-600 leading-relaxed verify-log-content" style="font-size: 11px;">{!! $entry->content !!}</div>
</div> </div>
@endif @endif
@endforeach @endforeach
@@ -268,6 +265,21 @@
/* Global scale factor for transcript page fonts. Adjust this easily (e.g. 1.0, 1.10, 1.20) */ /* Global scale factor for transcript page fonts. Adjust this easily (e.g. 1.0, 1.10, 1.20) */
--transcript-font-scale: 1.30; --transcript-font-scale: 1.30;
} }
.verify-log-content ul {
list-style-type: disc !important;
padding-left: 18px !important;
margin-top: 5px !important;
margin-bottom: 5px !important;
}
.verify-log-content ol {
list-style-type: decimal !important;
padding-left: 18px !important;
margin-top: 5px !important;
margin-bottom: 5px !important;
}
.verify-log-content li {
margin-bottom: 3px !important;
}
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;