15 Commits

Author SHA1 Message Date
Ümit Tunç 161d1c86bc feat: implement multilingual support for sitemap and SEO, update structured data, and improve locale middleware configuration 2026-07-04 00:59:55 +03:00
Ümit Tunç 4af8900a78 feat: add external admin dashboard link to navigation and remove Gantt chart widget from intern applications page 2026-07-02 10:40:10 +03:00
Ümit Tunç abcf105d07 feat: add intern panel navigation link and user password reset CLI command 2026-07-02 10:32:02 +03:00
Ümit Tunç d0292af20c feat: add GitHub username field and implement commit filtering by author in intern dashboard 2026-07-01 23:46:12 +03:00
Ümit Tunç 26670fcca5 feat: implement internship journal approval status tracking and add single-day printing functionality 2026-07-01 23:36:26 +03:00
Ümit Tunç f557d4b91c feat: replace eager in-memory GitHub commit fetching with on-demand retrieval and add a fallback UI for missing commits. 2026-07-01 23:34:42 +03:00
Ümit Tunç 09e92506fd feat: implement tabbed interface to toggle between Gantt chart and intern list in admin dashboard 2026-07-01 23:15:36 +03:00
Ümit Tunç 129dae4570 feat: add journal progress tracking UI for internship applications in the career dashboard 2026-07-01 23:06:13 +03:00
Ümit Tunç f0a9180a54 feat: add journal detail tracking, progress reporting, and notebook signing functionality for intern management 2026-07-01 23:04:23 +03:00
Ümit Tunç a6c250c13e refactor: simplify internship approval process by removing unit approval and adding supervisor name tracking 2026-07-01 22:37:08 +03:00
Ümit Tunç f84f78a138 feat: add approval system for internship journal entries including database fields, admin toggle functionality, and rich-text rendering 2026-07-01 21:58:56 +03:00
Ümit Tunç d9c1c825d9 style: increase container maximum width to 1440px in intern dashboard view 2026-07-01 17:49:06 +03:00
Ümit Tunç 13f8f09524 refactor: move tab logic and implement Quill editor and GitHub commit integration in scratch_test.js 2026-07-01 17:46:57 +03:00
Ümit Tunç 351b9064af feat: integrate Quill editor for internship journal entries, add retroactive field support, and enforce future date restrictions 2026-07-01 17:43:22 +03:00
Ümit Tunç edc843fa1c feat: restrict verification page view to accepted applications only 2026-07-01 17:40:06 +03:00
28 changed files with 2566 additions and 565 deletions
@@ -0,0 +1,77 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class ResetUserPassword extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:reset-password {email? : Kullanıcının e-posta adresi} {password? : Yeni şifre (Belirtilmezse etkileşimli olarak istenir veya otomatik oluşturulur)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Belirtilen kullanıcının şifresini günceller';
/**
* Execute the console command.
*/
public function handle(): int
{
// E-posta adresini al veya sor
$email = $this->argument('email') ?: $this->ask('Kullanıcının e-posta adresini giriniz:');
if (empty($email)) {
$this->error('E-posta adresi boş olamaz!');
return self::FAILURE;
}
// Email formatını kontrol et
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->error('Geçersiz e-posta formatı!');
return self::FAILURE;
}
// Kullanıcıyı bul
$user = User::where('email', $email)->first();
if (!$user) {
$this->error("E-posta adresi '{$email}' ile kayıtlı kullanıcı bulunamadı!");
return self::FAILURE;
}
// Şifreyi al veya sor ya da rastgele oluştur
$password = $this->argument('password');
if ($password === null) {
if ($this->confirm('Yeni şifreyi otomatik rastgele mi üretelim (6 haneli rakamsal)?', true)) {
$password = (string) random_int(100000, 999999);
} else {
$password = $this->secret('Yeni şifreyi giriniz:');
if (empty($password)) {
$this->error('Şifre boş olamaz!');
return self::FAILURE;
}
}
}
// Şifreyi güncelle ve kaydet
$user->password = Hash::make($password);
$user->save();
$this->info("✓ Kullanıcı '{$user->name}' ({$user->email}) şifresi başarıyla güncellendi!");
$this->info("Yeni Şifre: {$password}");
return self::SUCCESS;
}
}
@@ -199,22 +199,113 @@ class InternApplicationResource extends Resource
$saved = $record->journalEntries()->get()->keyBy('day_number'); $saved = $record->journalEntries()->get()->keyBy('day_number');
$html = '<div class="space-y-4" style="max-height: 400px; overflow-y: auto; padding-right: 10px; border: 1px solid #cbd5e1; border-radius: 8px; padding: 15px;">'; $html = '<style>
.rich-text-content p { margin-bottom: 8px; }
.rich-text-content ul { list-style-type: disc; padding-left: 20px; margin-bottom: 8px; }
.rich-text-content ol { list-style-type: decimal; padding-left: 20px; margin-bottom: 8px; }
.rich-text-content li { margin-bottom: 4px; }
</style>';
$html .= '<div class="space-y-4" style="max-height: 400px; overflow-y: auto; padding-right: 10px; border: 1px solid #cbd5e1; border-radius: 8px; padding: 15px;">';
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>';
$cleanContent = $content ? strip_tags($content, ['p', 'strong', 'ul', 'li', 'em', 'br', 'b', 'i', 'ol', 'span']) : '<em style="color:#94a3b8;">Rapor yazılmamış</em>';
$html .= ' <div class="rich-text-content" style="font-size:12px; color:#1e293b; line-height:1.5;">' . $cleanContent . '</div>';
if ($entry && trim($content) !== '') {
$supApproved = $entry->supervisor_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ı' . ($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>';
// Buttons
$supBtnText = $supApproved ? 'Onayı Kaldır' : 'Onayla';
$supBtnBg = $supApproved ? '#ef4444' : '#2563eb';
$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>';
}
$html .= '</div>'; $html .= '</div>';
} }
$html .= '</div>'; $html .= '</div>';
// JS handler
$html .= '
<script>
if (typeof window.toggleApproval !== "function") {
window.toggleApproval = function(entryId, btn) {
btn.disabled = true;
btn.style.opacity = "0.5";
fetch("' . route('intern.admin.toggle-journal-approval') . '", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "' . csrf_token() . '"
},
body: JSON.stringify({
entry_id: entryId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const badge = document.getElementById("sup-badge-" + entryId);
if (data.status) {
badge.style.background = "#d1fae5";
badge.style.color = "#065f46";
badge.textContent = "Sorumlu Onayladı (Onaylayan: " + data.supervisor_name + ")";
btn.textContent = "Sorumlu Onayı Kaldır";
btn.style.background = "#ef4444";
} else {
badge.style.background = "#f1f5f9";
badge.style.color = "#64748b";
badge.textContent = "Sorumlu Onayı Bekliyor";
btn.textContent = "Sorumlu Onayla";
btn.style.background = "#2563eb";
}
} else {
alert(data.message || "Bir hata oluştu.");
}
})
.catch(err => {
console.error(err);
alert("Bağlantı hatası oluştu.");
})
.finally(() => {
btn.disabled = false;
btn.style.opacity = "1";
});
};
}
</script>
';
// Add preview buttons // Add preview buttons
$html .= '<div style="margin-top: 15px; display: flex; gap: 10px;">'; $html .= '<div style="margin-top: 15px; display: flex; gap: 10px;">';
$html .= ' <a href="' . route('intern.print-journal') . '?size=a4&intern_id=' . $record->id . '" target="_blank" style="display:inline-flex; align-items:center; justify-content:center; padding: 8px 16px; background:#2563eb; color:white; border-radius:8px; font-weight:bold; font-size:12px; text-decoration:none; box-shadow: 0 1px 3px rgba(37, 99, 235, 0.2);">A4 Defteri Önizle / Yazdır</a>'; $html .= ' <a href="' . route('intern.print-journal') . '?size=a4&intern_id=' . $record->id . '" target="_blank" style="display:inline-flex; align-items:center; justify-content:center; padding: 8px 16px; background:#2563eb; color:white; border-radius:8px; font-weight:bold; font-size:12px; text-decoration:none; box-shadow: 0 1px 3px rgba(37, 99, 235, 0.2);">A4 Defteri Önizle / Yazdır</a>';
@@ -234,13 +325,6 @@ class InternApplicationResource extends Resource
->label('Staj Sorumlusu Adı / Ünvanı') ->label('Staj Sorumlusu Adı / Ünvanı')
->placeholder('Örn: Alperen Trunç') ->placeholder('Örn: Alperen Trunç')
->default('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') \Filament\Forms\Components\Toggle::make('notebook_approved')
->label('Staj Defterini Genel Olarak Onayla') ->label('Staj Defterini Genel Olarak Onayla')
->columnSpanFull(), ->columnSpanFull(),
@@ -426,10 +510,4 @@ class InternApplicationResource extends Resource
]; ];
} }
public static function getWidgets(): array
{
return [
Widgets\InternGanttChartWidget::class,
];
}
} }
@@ -8,11 +8,4 @@ use Filament\Resources\Pages\ListRecords;
class ListInternApplications extends ListRecords class ListInternApplications extends ListRecords
{ {
protected static string $resource = InternApplicationResource::class; protected static string $resource = InternApplicationResource::class;
protected function getHeaderWidgets(): array
{
return [
\App\Filament\Admin\Resources\InternApplications\Widgets\InternGanttChartWidget::class,
];
}
} }
@@ -1,175 +1,2 @@
<?php <?php
// Bu bileşen devre dışı bırakılmıştır.
namespace App\Filament\Admin\Resources\InternApplications\Widgets;
use App\Models\CareerApplication;
use Filament\Widgets\Widget;
use Carbon\Carbon;
class InternGanttChartWidget extends Widget
{
protected string $view = 'filament.admin.resources.intern-applications.widgets.intern-gantt-chart-widget';
protected int | string | array $columnSpan = 'full';
public function getViewData(): array
{
$interns = CareerApplication::query()
->where('type', 'internship')
->where('status', 'accepted')
->whereNotNull('internship_start_date')
->whereNotNull('internship_end_date')
->orderBy('internship_start_date', 'asc')
->get();
if ($interns->isEmpty()) {
return [
'interns' => [],
'months' => [],
'totalDays' => 0,
'minDate' => null,
'todayPercent' => null,
];
}
$minDate = Carbon::parse($interns->min('internship_start_date'))->startOfDay();
$maxDate = Carbon::parse($interns->max('internship_end_date'))->endOfDay();
// Pad the range slightly for better visual presentation
$minDate = $minDate->copy()->subDays(5);
$maxDate = $maxDate->copy()->addDays(5);
$totalDays = $minDate->diffInDays($maxDate);
if ($totalDays <= 0) {
$totalDays = 1;
}
// Generate months in range
$months = [];
$tempDate = $minDate->copy()->startOfMonth();
$endLimit = $maxDate->copy()->endOfMonth();
while ($tempDate->lte($endLimit)) {
$monthStart = $tempDate->copy()->startOfMonth();
$monthEnd = $tempDate->copy()->endOfMonth();
$overlapStart = $monthStart->gt($minDate) ? $monthStart : $minDate;
$overlapEnd = $monthEnd->lt($maxDate) ? $monthEnd : $maxDate;
if ($overlapStart->lte($overlapEnd)) {
$daysInOverlap = $overlapStart->diffInDays($overlapEnd) + 1;
$startOffset = $minDate->diffInDays($overlapStart);
$months[] = [
'name' => $tempDate->translatedFormat('F Y'),
'left' => ($startOffset / $totalDays) * 100,
'width' => ($daysInOverlap / $totalDays) * 100,
];
}
$tempDate->addMonth();
}
// Calculate today's line position if it falls within the range
$today = Carbon::today();
$todayPercent = null;
if ($today->between($minDate, $maxDate)) {
$todayPercent = ($minDate->diffInDays($today) / $totalDays) * 100;
}
$formattedInterns = $interns->map(function ($intern) use ($minDate, $totalDays) {
$start = Carbon::parse($intern->internship_start_date)->startOfDay();
$end = Carbon::parse($intern->internship_end_date)->endOfDay();
$startOffset = $minDate->diffInDays($start);
$duration = $start->diffInDays($end) + 1;
$left = ($startOffset / $totalDays) * 100;
$width = ($duration / $totalDays) * 100;
// Ensure width is at least a visible sliver
if ($width < 1.5) {
$width = 1.5;
}
// Assign a stable color palette based on the intern ID
$palettes = [
[
'bg' => 'bg-emerald-500 dark:bg-emerald-600',
'gradient' => 'from-emerald-400 to-teal-500 dark:from-emerald-500 dark:to-teal-600',
'text' => 'text-emerald-950 dark:text-emerald-50',
'border' => 'border-emerald-500/20',
'shadow' => 'shadow-emerald-500/20'
],
[
'bg' => 'bg-blue-500 dark:bg-blue-600',
'gradient' => 'from-blue-400 to-indigo-500 dark:from-blue-500 dark:to-indigo-600',
'text' => 'text-blue-950 dark:text-blue-50',
'border' => 'border-blue-500/20',
'shadow' => 'shadow-blue-500/20'
],
[
'bg' => 'bg-rose-500 dark:bg-rose-600',
'gradient' => 'from-rose-400 to-pink-500 dark:from-rose-500 dark:to-pink-600',
'text' => 'text-rose-950 dark:text-rose-50',
'border' => 'border-rose-500/20',
'shadow' => 'shadow-rose-500/20'
],
[
'bg' => 'bg-amber-500 dark:bg-amber-600',
'gradient' => 'from-amber-400 to-orange-500 dark:from-amber-500 dark:to-orange-600',
'text' => 'text-amber-950 dark:text-amber-50',
'border' => 'border-amber-500/20',
'shadow' => 'shadow-amber-500/20'
],
[
'bg' => 'bg-violet-500 dark:bg-violet-600',
'gradient' => 'from-violet-400 to-purple-500 dark:from-violet-500 dark:to-purple-600',
'text' => 'text-violet-950 dark:text-violet-50',
'border' => 'border-violet-500/20',
'shadow' => 'shadow-violet-500/20'
],
];
$paletteIndex = $intern->id % count($palettes);
$color = $palettes[$paletteIndex];
$startMonth = (int) $start->format('n');
$endMonth = (int) $end->format('n');
$gridStart = $startMonth + 1;
$gridEnd = min(14, $endMonth + 2);
// Background color map based on user's examples
$hexColors = [
'#2ecaac', // Emerald Teal
'#54c6f9', // Sky Blue
'#ff6252', // Red Coral
'#f59e0b', // Amber
'#8b5cf6', // Violet
];
$hexColor = $hexColors[$intern->id % count($hexColors)];
$isStripes = ($intern->id % 2 === 0);
return [
'id' => $intern->id,
'name' => $intern->name,
'start_date' => $start->translatedFormat('d M Y'),
'end_date' => $end->translatedFormat('d M Y'),
'total_days' => $intern->internship_total_days,
'left' => $left,
'width' => $width,
'color' => $color,
'grid_start' => $gridStart,
'grid_end' => $gridEnd,
'hex_color' => $hexColor,
'is_stripes' => $isStripes,
'raw_start' => $intern->internship_start_date,
'raw_end' => $intern->internship_end_date,
];
});
return [
'interns' => $formattedInterns,
'months' => $months,
'todayPercent' => $todayPercent,
];
}
}
+225 -3
View File
@@ -208,22 +208,35 @@ class CareerController extends Controller
$request->validate([ $request->validate([
'github_repo' => 'required|string|url|max:255', 'github_repo' => 'required|string|url|max:255',
'github_username' => 'nullable|string|max:100',
], [ ], [
'github_repo.required' => 'Lütfen Github depo URL\'sini girin.', 'github_repo.required' => 'Lütfen Github depo URL\'sini girin.',
'github_repo.url' => 'Geçerli bir URL girilmelidir.', 'github_repo.url' => 'Geçerli bir URL girilmelidir.',
'github_repo.max' => 'Github depo URL\'si en fazla 255 karakter olabilir.', 'github_repo.max' => 'Github depo URL\'si en fazla 255 karakter olabilir.',
'github_username.max' => 'Github kullanıcı adı en fazla 100 karakter olabilir.',
]); ]);
$intern = CareerApplication::findOrFail(session('intern_id')); $intern = CareerApplication::findOrFail(session('intern_id'));
// Sanitize & format github URL // Sanitize & format github URL
$repoUrl = trim($request->github_repo); $repoUrl = trim($request->github_repo);
$username = trim($request->github_username);
$intern->update([ $intern->update([
'github_repo' => $repoUrl 'github_repo' => $repoUrl,
'github_username' => $username ?: null,
]); ]);
return redirect()->back()->with('success', 'Github deposu başarıyla güncellendi.'); if ($request->expectsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'message' => 'Github bilgileri başarıyla güncellendi.',
'github_repo' => $repoUrl,
'github_username' => $username ?: null,
]);
}
return redirect()->back()->with('success', 'Github bilgileri başarıyla güncellendi.');
} }
public function downloadMarkdown() public function downloadMarkdown()
@@ -393,6 +406,7 @@ class CareerController extends Controller
} }
$allInterns = CareerApplication::where('type', 'internship') $allInterns = CareerApplication::where('type', 'internship')
->with(['journalEntries'])
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->get(); ->get();
@@ -472,21 +486,34 @@ 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,
'supervisor_approved' => false,
'supervisor_name' => null,
]); ]);
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
]); ]);
} }
@@ -509,6 +536,15 @@ class CareerController extends Controller
} }
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days); $days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
$dayFilter = $request->query('day');
if ($dayFilter) {
$days = array_filter($days, function($d) use ($dayFilter) {
return $d['day_number'] == $dayFilter;
});
$days = array_values($days);
}
$savedEntries = $intern->journalEntries()->get()->keyBy('day_number'); $savedEntries = $intern->journalEntries()->get()->keyBy('day_number');
return view('front.career.print', [ return view('front.career.print', [
@@ -518,5 +554,191 @@ class CareerController extends Controller
'size' => $size, 'size' => $size,
]); ]);
} }
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()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'entry_id' => 'required|integer|exists:internship_journal_entries,id',
]);
$entry = \App\Models\InternshipJournalEntry::findOrFail($request->entry_id);
$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.'
]);
}
public function getInternJournalDetails(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',
]);
$intern = CareerApplication::findOrFail($request->intern_id);
// Get all days calculated
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
// Get saved journal entries
$savedEntries = $intern->journalEntries()->get()->keyBy('day_number');
// Prepare entries list matched by day number
$entries = [];
$filledDaysCount = 0;
foreach ($days as $d) {
$dayNum = $d['day_number'];
$entry = $savedEntries->get($dayNum);
$hasSaved = $entry && trim($entry->content) !== '';
if ($hasSaved) {
$filledDaysCount++;
}
$entries[] = [
'day_number' => $dayNum,
'date' => $d['date'],
'formatted_date' => $d['formatted_date'],
'filled' => $hasSaved,
'entry_id' => $entry ? $entry->id : null,
'content' => $entry ? $entry->content : null,
'is_retroactive' => $entry ? $entry->is_retroactive : false,
'supervisor_approved' => $entry ? (bool)$entry->supervisor_approved : false,
'supervisor_name' => $entry ? $entry->supervisor_name : null,
];
}
return response()->json([
'success' => true,
'intern' => [
'id' => $intern->id,
'name' => $intern->name,
'email' => $intern->email,
'phone' => $intern->phone,
'start_date' => $intern->internship_start_date,
'end_date' => $intern->internship_end_date,
'total_days' => $intern->internship_total_days,
'filled_days' => $filledDaysCount,
'notebook_supervisor_signed' => (bool)$intern->notebook_supervisor_signed,
'notebook_supervisor_name' => $intern->notebook_supervisor_name,
'notebook_unit_signed' => (bool)$intern->notebook_unit_signed,
'notebook_unit_name' => $intern->notebook_unit_name,
'notebook_approved' => (bool)$intern->notebook_approved,
],
'entries' => $entries,
]);
}
public function toggleNotebookSignature(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',
'type' => 'required|string|in:supervisor,unit,approved',
'signed' => 'required|boolean',
'name' => 'nullable|string|max:255',
]);
$intern = CareerApplication::findOrFail($request->intern_id);
$type = $request->type;
$signed = $request->signed;
$name = $request->name ?: auth()->user()->name;
if ($type === 'supervisor') {
$intern->notebook_supervisor_signed = $signed;
$intern->notebook_supervisor_name = $signed ? $name : null;
} elseif ($type === 'unit') {
$intern->notebook_unit_signed = $signed;
$intern->notebook_unit_name = $signed ? $name : null;
} elseif ($type === 'approved') {
$intern->notebook_approved = $signed;
}
$intern->save();
return response()->json([
'success' => true,
'message' => 'Staj defteri onay durumu güncellendi.',
'intern' => [
'notebook_supervisor_signed' => (bool)$intern->notebook_supervisor_signed,
'notebook_supervisor_name' => $intern->notebook_supervisor_name,
'notebook_unit_signed' => (bool)$intern->notebook_unit_signed,
'notebook_unit_name' => $intern->notebook_unit_name,
'notebook_approved' => (bool)$intern->notebook_approved,
]
]);
}
} }
+38 -6
View File
@@ -12,24 +12,50 @@ class SitemapController extends Controller
public function index(): Response public function index(): Response
{ {
$urls = []; $urls = [];
$activeLanguages = [];
if (class_exists(\App\Models\Language::class)) {
$activeLanguages = \App\Models\Language::where('is_active', true)->get();
}
$getAlternates = function (string $baseUrl) use ($activeLanguages) {
$alternates = [];
foreach ($activeLanguages as $lang) {
$connector = str_contains($baseUrl, '?') ? '&' : '?';
$alternates[] = [
'hreflang' => $lang->code,
'href' => $baseUrl . $connector . 'locale=' . $lang->code,
];
}
$alternates[] = [
'hreflang' => 'x-default',
'href' => $baseUrl,
];
return $alternates;
};
// 1. Static & Main Pages // 1. Static & Main Pages
$homeUrl = url('/');
$urls[] = [ $urls[] = [
'loc' => url('/'), 'loc' => $homeUrl,
'alternates' => $getAlternates($homeUrl),
'lastmod' => now()->startOfDay()->toAtomString(), 'lastmod' => now()->startOfDay()->toAtomString(),
'changefreq' => 'daily', 'changefreq' => 'daily',
'priority' => '1.0', 'priority' => '1.0',
]; ];
$blogIndexUrl = route('blog.index');
$urls[] = [ $urls[] = [
'loc' => route('blog.index'), 'loc' => $blogIndexUrl,
'alternates' => $getAlternates($blogIndexUrl),
'lastmod' => now()->startOfDay()->toAtomString(), 'lastmod' => now()->startOfDay()->toAtomString(),
'changefreq' => 'weekly', 'changefreq' => 'weekly',
'priority' => '0.8', 'priority' => '0.8',
]; ];
$careerIndexUrl = route('career.index');
$urls[] = [ $urls[] = [
'loc' => route('career.index'), 'loc' => $careerIndexUrl,
'alternates' => $getAlternates($careerIndexUrl),
'lastmod' => now()->startOfMonth()->toAtomString(), 'lastmod' => now()->startOfMonth()->toAtomString(),
'changefreq' => 'monthly', 'changefreq' => 'monthly',
'priority' => '0.5', 'priority' => '0.5',
@@ -40,8 +66,10 @@ class SitemapController extends Controller
->where('is_homepage', false) ->where('is_homepage', false)
->get(); ->get();
foreach ($pages as $page) { foreach ($pages as $page) {
$pageUrl = url($page->slug);
$urls[] = [ $urls[] = [
'loc' => url($page->slug), 'loc' => $pageUrl,
'alternates' => $getAlternates($pageUrl),
'lastmod' => $page->updated_at->toAtomString(), 'lastmod' => $page->updated_at->toAtomString(),
'changefreq' => 'weekly', 'changefreq' => 'weekly',
'priority' => '0.7', 'priority' => '0.7',
@@ -52,8 +80,10 @@ class SitemapController extends Controller
if (class_exists(Blog::class)) { if (class_exists(Blog::class)) {
$posts = Blog::published()->get(); $posts = Blog::published()->get();
foreach ($posts as $post) { foreach ($posts as $post) {
$postUrl = route('blog.show', $post->slug);
$urls[] = [ $urls[] = [
'loc' => route('blog.show', $post->slug), 'loc' => $postUrl,
'alternates' => $getAlternates($postUrl),
'lastmod' => $post->updated_at->toAtomString(), 'lastmod' => $post->updated_at->toAtomString(),
'changefreq' => 'weekly', 'changefreq' => 'weekly',
'priority' => '0.6', 'priority' => '0.6',
@@ -65,8 +95,10 @@ class SitemapController extends Controller
if (class_exists(Product::class)) { if (class_exists(Product::class)) {
$products = Product::where('is_active', true)->get(); $products = Product::where('is_active', true)->get();
foreach ($products as $product) { foreach ($products as $product) {
$productUrl = route('products.show', $product->slug);
$urls[] = [ $urls[] = [
'loc' => route('products.show', $product->slug), 'loc' => $productUrl,
'alternates' => $getAlternates($productUrl),
'lastmod' => $product->updated_at->toAtomString(), 'lastmod' => $product->updated_at->toAtomString(),
'changefreq' => 'weekly', 'changefreq' => 'weekly',
'priority' => '0.7', 'priority' => '0.7',
+19 -11
View File
@@ -16,10 +16,25 @@ class SetLocale
*/ */
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
// Session'dan locale'i al, yoksa varsayılan dil kodunu kullan // Aktif dilleri veritabanından kontrol et
$locale = session('locale'); if (function_exists('available_language_codes')) {
$availableLocales = available_language_codes();
} else {
$availableLocales = ['tr', 'en', 'de', 'ar', 'se', 'ru'];
}
// Query parametresinden al, yoksa session'dan al
$queryLocale = $request->query('locale') ?? $request->query('lang');
$locale = null;
if ($queryLocale && in_array($queryLocale, $availableLocales)) {
$locale = $queryLocale;
session(['locale' => $locale]);
} else {
$locale = session('locale');
}
// Eğer session'da locale yoksa, varsayılan dil kodunu kullan // Eğer locale hala atanmadıysa varsayılan dil kodunu kullan
if (!$locale) { if (!$locale) {
if (function_exists('default_language_code')) { if (function_exists('default_language_code')) {
$locale = default_language_code(); $locale = default_language_code();
@@ -28,14 +43,7 @@ class SetLocale
} }
} }
// Aktif dilleri veritabanından kontrol et // Locale'i aktif diller arasında kontrol et ve ata
if (function_exists('available_language_codes')) {
$availableLocales = available_language_codes();
} else {
$availableLocales = ['tr', 'en'];
}
// Locale'i aktif diller arasında kontrol et
if (in_array($locale, $availableLocales)) { if (in_array($locale, $availableLocales)) {
App::setLocale($locale); App::setLocale($locale);
} else { } else {
+1
View File
@@ -30,6 +30,7 @@ class CareerApplication extends Model
'internship_end_date', 'internship_end_date',
'internship_total_days', 'internship_total_days',
'github_repo', 'github_repo',
'github_username',
'certificate_code', 'certificate_code',
'transcript_markdown', 'transcript_markdown',
'notebook_supervisor_signed', 'notebook_supervisor_signed',
+10
View File
@@ -14,6 +14,16 @@ class InternshipJournalEntry extends Model
'day_number', 'day_number',
'date', 'date',
'content', 'content',
'is_retroactive',
'supervisor_approved',
'unit_approved',
'supervisor_name',
];
protected $casts = [
'is_retroactive' => 'boolean',
'supervisor_approved' => 'boolean',
'unit_approved' => 'boolean',
]; ];
/** /**
@@ -85,6 +85,13 @@ class AdminPanelProvider extends PanelProvider
->sort(100); // En sonda görünsün ->sort(100); // En sonda görünsün
})->toArray() })->toArray()
]) ])
->navigationItems([
\Filament\Navigation\NavigationItem::make('Admin Stajyer Paneli')
->url('https://truncgil.com/stajyer/admin/panel')
->openUrlInNewTab()
->icon('heroicon-o-academic-cap')
->sort(1000),
])
->assets([ ->assets([
\Filament\Support\Assets\Css::make('citrus', resource_path('css/citrus.css')), \Filament\Support\Assets\Css::make('citrus', resource_path('css/citrus.css')),
]); ]);
+37 -1
View File
@@ -63,10 +63,42 @@ abstract class StructuredData
$siteName = static::siteName(); $siteName = static::siteName();
$node = [ $node = [
'@type' => 'Organization', '@type' => 'ProfessionalService',
'@id' => $siteUrl . '#organization', '@id' => $siteUrl . '#organization',
'name' => $siteName, 'name' => $siteName,
'url' => $siteUrl, 'url' => $siteUrl,
'priceRange' => '$$',
'geo' => [
'@type' => 'GeoCoordinates',
'latitude' => 37.025704,
'longitude' => 37.296559,
],
'areaServed' => [
[
'@type' => 'AdministrativeArea',
'name' => 'Gaziantep',
],
[
'@type' => 'Country',
'name' => 'Turkey',
]
],
'knowsAbout' => [
'Yazılım Geliştirme',
'Web Tasarım',
'Mobil Uygulama Geliştirme',
'E-Ticaret Sistemleri',
'SEO Danışmanlığı',
'Gaziantep Yazılım Şirketleri'
],
'openingHoursSpecification' => [
[
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
'opens' => '09:00',
'closes' => '18:00',
]
]
]; ];
$logo = setting('site_logo'); $logo = setting('site_logo');
@@ -93,6 +125,10 @@ abstract class StructuredData
$node['address'] = [ $node['address'] = [
'@type' => 'PostalAddress', '@type' => 'PostalAddress',
'streetAddress' => $address, 'streetAddress' => $address,
'addressLocality' => 'Şahinbey',
'addressRegion' => 'Gaziantep',
'postalCode' => '27190',
'addressCountry' => 'TR',
]; ];
} }
@@ -0,0 +1,29 @@
<?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('supervisor_approved')->default(false);
$table->boolean('unit_approved')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->dropColumn(['supervisor_approved', 'unit_approved']);
});
}
};
@@ -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');
});
}
};
@@ -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');
});
}
};
@@ -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('career_applications', function (Blueprint $table) {
$table->string('github_username')->nullable()->after('github_repo');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->dropColumn('github_username');
});
}
};
@@ -0,0 +1 @@
google-site-verification: googlef1M6GQLxVA5g7IaVY6kMQCoIrgADR6HjrOZu79CrXYc.html
+5 -1
View File
@@ -1,2 +1,6 @@
User-agent: * User-agent: *
Disallow: Allow: /
Disallow: /admin/
Disallow: /stajyer/admin/
Sitemap: https://truncgil.com/sitemap.xml
@@ -1,157 +1 @@
<x-filament-widgets::widget> {{-- Bu bileşen görünümü devre dışı bırakılmıştır. --}}
<!-- DevExtreme Gantt CSS Dependencies -->
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.5/css/dx.light.css">
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.5/css/dx-gantt.css">
<style>
.fi-wi-intern-gantt-chart .demo-container {
height: 560px;
background-color: #fff;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0.05);
margin-top: 12px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
}
.dark .fi-wi-intern-gantt-chart .demo-container {
background-color: #111827;
border-color: rgba(255, 255, 255, 0.05);
}
.fi-wi-intern-gantt-chart #gantt {
height: 100%;
width: 100%;
}
</style>
<x-filament::section class="fi-wi-intern-gantt-chart">
<x-slot name="heading">
<div class="flex items-center justify-between">
<span class="text-lg font-bold tracking-tight text-gray-950 dark:text-white">
Stajyer Gantt Şeması ve Takvimi
</span>
<span class="text-xs px-2.5 py-1 bg-emerald-50 text-emerald-600 dark:bg-emerald-950 dark:text-emerald-400 rounded-full font-semibold">
DevExtreme Görünümü
</span>
</div>
</x-slot>
@if(count($interns) === 0)
<div class="flex flex-col items-center justify-center p-8 text-center bg-gray-50 dark:bg-gray-900/40 rounded-xl border border-dashed border-gray-200 dark:border-gray-800">
<div class="p-3 bg-gray-100 dark:bg-gray-800 rounded-full mb-3 text-gray-400">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" />
</svg>
</div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">
Tarihleri belirlenmiş onaylı stajyer bulunamadı.
</p>
</div>
@else
<div
x-data="{
internsData: @js($interns),
loadScript(src) {
return new Promise((resolve, reject) => {
if (window.jQuery && src.includes('jquery')) { resolve(); return; }
if (window.DevExpress && src.includes('dx.all')) { resolve(); return; }
if (window.DevExpress && window.DevExpress.ui && window.DevExpress.ui.dxGantt && src.includes('dx-gantt')) { resolve(); return; }
const existing = document.querySelector(`script[src='${src}']`);
if (existing) {
if (existing.readyState === 'loaded' || existing.readyState === 'complete') {
resolve();
} else {
existing.addEventListener('load', resolve);
existing.addEventListener('error', reject);
}
return;
}
const script = document.createElement('script');
script.src = src;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('Failed to load script: ' + src));
document.head.appendChild(script);
});
},
showError(msg) {
const errDiv = document.createElement('div');
errDiv.style.color = '#ef4444';
errDiv.style.padding = '20px';
errDiv.style.fontFamily = 'monospace';
errDiv.style.fontSize = '13px';
errDiv.innerHTML = '<strong>Gantt Load Error:</strong><br>' + msg;
this.$refs.ganttContainer.innerHTML = '';
this.$refs.ganttContainer.appendChild(errDiv);
},
async init() {
try {
await this.loadScript('https://code.jquery.com/jquery-3.7.1.min.js').catch(() => { throw new Error('jQuery failed to load'); });
await this.loadScript('https://cdn3.devexpress.com/jslib/23.2.5/js/dx.all.js').catch(() => { throw new Error('dx.all.js failed to load'); });
await this.loadScript('https://cdn3.devexpress.com/jslib/23.2.5/js/dx-gantt.min.js').catch(() => { throw new Error('dx-gantt.min.js failed to load'); });
if (window.jQuery && window.jQuery.fn.dxGantt) {
const formattedData = this.internsData.map(intern => ({
id: intern.id,
parentId: 0,
title: intern.name,
start: new Date(intern.raw_start),
end: new Date(intern.raw_end),
progress: 100
}));
jQuery(this.$refs.ganttContainer).dxGantt({
tasks: {
dataSource: formattedData,
},
editing: {
enabled: false,
},
validation: {
autoUpdateParentTasks: true,
},
toolbar: {
items: [
'collapseAll',
'expandAll',
'separator',
'zoomIn',
'zoomOut',
],
},
columns: [{
dataField: 'title',
caption: 'Stajyer Adı',
width: 250,
}, {
dataField: 'start',
caption: 'Başlangıç Tarihi',
dataType: 'date',
format: 'dd.MM.yyyy',
width: 120,
}, {
dataField: 'end',
caption: 'Bitiş Tarihi',
dataType: 'date',
format: 'dd.MM.yyyy',
width: 120,
}],
scaleType: 'days',
taskListWidth: 490,
});
} else {
throw new Error('DevExpress Gantt component jQuery plugin is not defined after loading scripts.');
}
} catch (e) {
console.error(e);
this.showError(e.message);
}
}
}"
class="dx-viewport demo-container"
>
<div id="gantt" x-ref="ganttContainer" style="height: 100%; width: 100%;"></div>
</div>
@endif
</x-filament::section>
</x-filament-widgets::widget>
@@ -11,15 +11,632 @@
<!-- Statistics Section --> <!-- Statistics Section -->
@include('front.career.partials.stats') @include('front.career.partials.stats')
<!-- Gantt Chart Section --> <!-- Tab Switcher Menu -->
@include('front.career.partials.gantt') <div class="bg-white rounded-3xl p-4 shadow-xl border border-slate-100/50 mb-8">
<div class="flex flex-col sm:flex-row gap-3" role="tablist">
<button type="button" role="tab" aria-selected="true" data-tab-target="gantt-chart" class="admin-tab-btn active 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 cursor-pointer">
<i class="uil uil-schedule text-lg"></i>
<span>Gantt Şeması ve Takvimi</span>
</button>
<button type="button" role="tab" aria-selected="false" data-tab-target="intern-list" 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">
<i class="uil uil-list-ul text-lg"></i>
<span>Stajyer Listesi ve Detaylı Takip</span>
</button>
</div>
</div>
<!-- Interns Table / Detailed List Section --> <!-- Tab Panels -->
@include('front.career.partials.list') <div id="gantt-chart-panel" class="admin-tab-panel space-y-6">
@include('front.career.partials.gantt')
</div>
<div id="intern-list-panel" class="admin-tab-panel hidden space-y-6">
@include('front.career.partials.list')
</div>
</div> </div>
</div> </div>
</section> </section>
@include('front.career.partials.guide_modal') @include('front.career.partials.guide_modal')
<!-- Intern Journal Tracking & Approval Modal -->
<div id="intern-journal-modal" class="fixed inset-0 z-[9998] 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-4xl w-full mx-4 overflow-hidden transform scale-95 opacity-0 transition-all duration-300 flex flex-col max-h-[90vh]">
<!-- 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="ijm-name" class="font-bold text-slate-800 text-lg">Stajyer Defteri ve İnceleme</h4>
<p id="ijm-meta" class="text-xs text-slate-500 font-semibold mt-0.5"></p>
</div>
<button type="button" onclick="closeInternJournalModal()" 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 grid grid-cols-1 md:grid-cols-12 gap-6">
<!-- Left Sidebar: Progress & General Approvals (col-span-4) -->
<div class="md:col-span-4 space-y-6">
<!-- Progress Bar Card -->
<div class="bg-slate-50 rounded-2xl p-5 border border-slate-200/50">
<h5 class="font-bold text-slate-700 text-xs uppercase tracking-wider mb-4 flex items-center gap-1.5">
<i class="uil uil-chart-bar text-blue-600"></i>
<span>Staj İlerlemesi</span>
</h5>
<!-- Journal Filling Progress -->
<div class="space-y-2 mb-4">
<div class="flex justify-between text-xs font-bold">
<span class="text-slate-500">Defter Doldurma</span>
<span id="ijm-fill-text" class="text-slate-700">0 / 0 Gün</span>
</div>
<div class="w-full bg-slate-200 h-2.5 rounded-full overflow-hidden">
<div id="ijm-fill-progress" class="bg-blue-600 h-full rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
<!-- Journal Approval Progress -->
<div class="space-y-2">
<div class="flex justify-between text-xs font-bold">
<span class="text-slate-500">Onaylı Günler</span>
<span id="ijm-approved-text" class="text-slate-700">0 / 0 Gün</span>
</div>
<div class="w-full bg-slate-200 h-2.5 rounded-full overflow-hidden">
<div id="ijm-approved-progress" class="bg-emerald-500 h-full rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Overall Signatures Card -->
<div class="bg-slate-50 rounded-2xl p-5 border border-slate-200/50 space-y-4">
<h5 class="font-bold text-slate-700 text-xs uppercase tracking-wider flex items-center gap-1.5">
<i class="uil uil-signature text-indigo-600"></i>
<span>Resmi Onaylar / İmzalar</span>
</h5>
<!-- Supervisor Signature -->
<div class="p-3 bg-white rounded-xl border border-slate-100 flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-600">Sorumlu İmzası</span>
<button type="button" id="ijm-btn-supervisor" onclick="toggleNotebookSignature('supervisor')" class="px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border transition-all"></button>
</div>
<div class="text-[10px] text-slate-400 font-semibold" id="ijm-text-supervisor">İmzalanmadı</div>
</div>
<!-- Unit Signature -->
<div class="p-3 bg-white rounded-xl border border-slate-100 flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-600">Birim Sorumlu İmzası</span>
<button type="button" id="ijm-btn-unit" onclick="toggleNotebookSignature('unit')" class="px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border transition-all"></button>
</div>
<div class="text-[10px] text-slate-400 font-semibold" id="ijm-text-unit">İmzalanmadı</div>
</div>
<!-- Notebook Approved -->
<div class="p-3 bg-white rounded-xl border border-slate-100 flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-600">Genel Defter Onayı</span>
<button type="button" id="ijm-btn-approved" onclick="toggleNotebookSignature('approved')" class="px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border transition-all"></button>
</div>
<div class="text-[10px] text-slate-400 font-semibold" id="ijm-text-approved">Onaylanmadı</div>
</div>
</div>
</div>
<!-- Right Section: Carousel & Navigation (col-span-8) -->
<div class="md:col-span-8 flex flex-col space-y-4">
<!-- Carousel Controls -->
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-200/50 flex items-center justify-between gap-3">
<button type="button" onclick="prevSlide()" class="p-2 bg-white hover:bg-slate-100 border border-slate-200 text-slate-600 hover:text-slate-900 rounded-xl transition-all flex items-center justify-center cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"/></svg>
</button>
<div class="flex-grow flex items-center justify-center gap-3">
<span id="ijm-day-indicator" class="text-sm font-extrabold text-blue-600 bg-blue-50 px-3 py-1.5 rounded-xl">Gün: 0 / 0</span>
<!-- Quick Day Dropdown -->
<select id="ijm-day-select" onchange="goToSlide(this.value)" class="text-xs font-semibold text-slate-700 bg-white border border-slate-200 rounded-xl px-2.5 py-1.5 focus:outline-none focus:border-blue-400">
<!-- Options loaded dynamically -->
</select>
</div>
<button type="button" onclick="nextSlide()" class="p-2 bg-white hover:bg-slate-100 border border-slate-200 text-slate-600 hover:text-slate-900 rounded-xl transition-all flex items-center justify-center cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
</button>
</div>
<!-- Carousel Slide Container -->
<div class="bg-white rounded-2xl border border-slate-100 p-6 flex-grow flex flex-col shadow-sm min-h-[300px]">
<!-- Day Title & Date -->
<div class="flex items-center justify-between pb-3 border-b border-slate-100 mb-4 flex-shrink-0">
<div>
<span id="ijm-slide-title" class="text-sm font-extrabold text-slate-800">Seçili Gün</span>
<span id="ijm-slide-date" class="text-xs font-semibold text-slate-400 ml-2"></span>
</div>
<div id="ijm-slide-retroactive">
<!-- Retroactive badge -->
</div>
</div>
<!-- Slide Content (Rich text HTML) -->
<div class="flex-grow overflow-y-auto no-scrollbar prose max-w-none text-slate-700 leading-relaxed text-sm" style="max-height: 250px;">
<div id="ijm-slide-content">
<!-- Day content loaded here -->
</div>
</div>
<!-- Slide Actions / Daily approval status -->
<div class="pt-4 border-t border-slate-100 flex items-center justify-between mt-4 flex-shrink-0">
<div id="ijm-slide-status-badge">
<!-- Status badge -->
</div>
<button type="button" id="ijm-slide-action-btn" onclick="toggleActiveDayApproval()" class="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 cursor-pointer">
Onayla
</button>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 bg-slate-50 border-t border-slate-100 flex items-center justify-end flex-shrink-0">
<button type="button" onclick="closeInternJournalModal()" class="px-5 py-2.5 bg-slate-800 hover:bg-slate-900 text-white font-bold rounded-xl text-xs transition-colors shadow-md cursor-pointer">Kapat</button>
</div>
</div>
</div>
@push('styles')
<style>
.admin-tab-btn {
transition: all 0.2s ease-in-out;
}
.admin-tab-btn:hover:not(.active) {
background-color: #f8fafc;
}
.admin-tab-btn.active {
background-color: #eff6ff;
border-color: #dbeafe;
color: #1e40af !important;
}
</style>
@endpush
@push('scripts')
<script>
let currentInternId = null;
let journalEntries = [];
let activeSlideIndex = 0;
let isUpdatingSignature = false;
let isUpdatingDayApproval = false;
function openInternJournalModal(internId) {
currentInternId = internId;
activeSlideIndex = 0;
const modal = document.getElementById('intern-journal-modal');
if (!modal) return;
modal.classList.remove('hidden');
modal.classList.add('flex');
// Animate open
const card = modal.querySelector('.max-w-4xl');
if (card) {
setTimeout(() => {
card.classList.remove('scale-95', 'opacity-0');
card.classList.add('scale-100', 'opacity-100');
}, 50);
}
loadInternJournalDetails(internId);
}
function closeInternJournalModal() {
const modal = document.getElementById('intern-journal-modal');
if (!modal) return;
const card = modal.querySelector('.max-w-4xl');
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 loadInternJournalDetails(internId) {
const slideContent = document.getElementById('ijm-slide-content');
if (slideContent) {
slideContent.innerHTML = `
<div class="flex items-center justify-center p-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
`;
}
fetch(`/stajyer/admin/journal-details?intern_id=${internId}`)
.then(res => res.json())
.then(data => {
if (!data.success) {
alert(data.message || 'Veriler yüklenemedi.');
closeInternJournalModal();
return;
}
// Set Header details
const nameEl = document.getElementById('ijm-name');
if (nameEl) nameEl.textContent = data.intern.name;
let startF = data.intern.start_date ? formatDateStr(data.intern.start_date) : '-';
let endF = data.intern.end_date ? formatDateStr(data.intern.end_date) : '-';
const metaEl = document.getElementById('ijm-meta');
if (metaEl) metaEl.textContent = `${startF} - ${endF} (${data.intern.total_days} İş Günü)`;
// Setup Progress Bars
const fillPercent = data.intern.total_days > 0 ? (data.intern.filled_days / data.intern.total_days) * 100 : 0;
const fillTextEl = document.getElementById('ijm-fill-text');
if (fillTextEl) fillTextEl.textContent = `${data.intern.filled_days} / ${data.intern.total_days} Gün`;
const fillBarEl = document.getElementById('ijm-fill-progress');
if (fillBarEl) fillBarEl.style.width = `${fillPercent}%`;
// Approved days count
let approvedDaysCount = 0;
data.entries.forEach(e => {
if (e.filled && e.supervisor_approved) {
approvedDaysCount++;
}
});
const approvedPercent = data.intern.total_days > 0 ? (approvedDaysCount / data.intern.total_days) * 100 : 0;
const appTextEl = document.getElementById('ijm-approved-text');
if (appTextEl) appTextEl.textContent = `${approvedDaysCount} / ${data.intern.total_days} Gün`;
const appBarEl = document.getElementById('ijm-approved-progress');
if (appBarEl) appBarEl.style.width = `${approvedPercent}%`;
// Setup Signatures
updateSignatureUI('supervisor', data.intern.notebook_supervisor_signed, data.intern.notebook_supervisor_name);
updateSignatureUI('unit', data.intern.notebook_unit_signed, data.intern.notebook_unit_name);
updateSignatureUI('approved', data.intern.notebook_approved, null);
// Setup carousel slide data
journalEntries = data.entries;
// Setup dropdown
const select = document.getElementById('ijm-day-select');
if (select) {
select.innerHTML = '';
journalEntries.forEach((entry, idx) => {
const opt = document.createElement('option');
opt.value = idx;
opt.textContent = `${entry.day_number}. Gün (${entry.formatted_date})` + (entry.filled ? ' [DOLU]' : ' [BOŞ]');
select.appendChild(opt);
});
}
// Set total indicator text
const indEl = document.getElementById('ijm-day-indicator');
if (indEl) indEl.textContent = `Gün: 1 / ${journalEntries.length}`;
// Render first slide
renderSlide(0);
})
.catch(err => {
console.error(err);
alert('Staj detayları yüklenirken bir hata oluştu.');
closeInternJournalModal();
});
}
function formatDateStr(dateStr) {
if (!dateStr) return '';
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[2]}.${parts[1]}.${parts[0]}`;
}
return dateStr;
}
function updateSignatureUI(type, signed, name) {
const btn = document.getElementById(`ijm-btn-${type}`);
const label = document.getElementById(`ijm-text-${type}`);
if (!btn || !label) return;
if (type === 'supervisor') {
if (signed) {
btn.textContent = "İmzayı Kaldır";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-rose-200 bg-rose-50 text-rose-600 hover:bg-rose-100 transition-all cursor-pointer";
label.innerHTML = `<span class="text-green-600 font-extrabold"><i class="uil uil-check-circle mr-0.5"></i>İmzaladı: ${name || ''}</span>`;
} else {
btn.textContent = "İmzala";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-indigo-200 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 transition-all cursor-pointer";
label.textContent = "İmzalanmadı";
}
} else if (type === 'unit') {
if (signed) {
btn.textContent = "İmzayı Kaldır";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-rose-200 bg-rose-50 text-rose-600 hover:bg-rose-100 transition-all cursor-pointer";
label.innerHTML = `<span class="text-green-600 font-extrabold"><i class="uil uil-check-circle mr-0.5"></i>İmzaladı: ${name || ''}</span>`;
} else {
btn.textContent = "İmzala";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-indigo-200 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 transition-all cursor-pointer";
label.textContent = "İmzalanmadı";
}
} else if (type === 'approved') {
if (signed) {
btn.textContent = "Onayı Kaldır";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-rose-200 bg-rose-50 text-rose-600 hover:bg-rose-100 transition-all cursor-pointer";
label.innerHTML = `<span class="text-green-600 font-extrabold"><i class="uil uil-check-circle mr-0.5"></i>Defter Onaylandı</span>`;
} else {
btn.textContent = "Onayla";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-emerald-200 bg-emerald-50 text-emerald-600 hover:bg-emerald-100 transition-all cursor-pointer";
label.textContent = "Onaylanmadı";
}
}
}
function toggleNotebookSignature(type) {
if (isUpdatingSignature) return;
// Get current state
const btn = document.getElementById(`ijm-btn-${type}`);
if (!btn) return;
const isSignedCurrently = btn.textContent.includes('Kaldır');
const newSignState = !isSignedCurrently;
let promptName = '';
if (newSignState && (type === 'supervisor' || type === 'unit')) {
promptName = prompt("İmzalayan yetkili ismini giriniz veya onaylayınız:", @json(auth()->user()->name));
if (promptName === null) return; // cancelled
if (promptName.trim() === '') {
promptName = @json(auth()->user()->name);
}
}
isUpdatingSignature = true;
btn.style.opacity = '0.5';
fetch('/stajyer/admin/toggle-notebook-signature', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
intern_id: currentInternId,
type: type,
signed: newSignState ? 1 : 0,
name: promptName
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
const key = type === 'supervisor' ? 'notebook_supervisor_signed' : (type === 'unit' ? 'notebook_unit_signed' : 'notebook_approved');
const nameKey = type === 'supervisor' ? 'notebook_supervisor_name' : (type === 'unit' ? 'notebook_unit_name' : null);
updateSignatureUI(type, data.intern[key], data.intern[nameKey]);
} else {
alert(data.message || 'Hata oluştu.');
}
})
.catch(err => {
console.error(err);
alert('İşlem gerçekleştirilemedi.');
})
.finally(() => {
isUpdatingSignature = false;
btn.style.opacity = '1';
});
}
function renderSlide(index) {
if (index < 0 || index >= journalEntries.length) return;
activeSlideIndex = index;
const entry = journalEntries[index];
// Update indicator & select dropdown
const indEl = document.getElementById('ijm-day-indicator');
if (indEl) indEl.textContent = `Gün: ${index + 1} / ${journalEntries.length}`;
const selEl = document.getElementById('ijm-day-select');
if (selEl) selEl.value = index;
// Title & Date
const titleEl = document.getElementById('ijm-slide-title');
if (titleEl) titleEl.textContent = `${entry.day_number}. Gün Raporu`;
const dateEl = document.getElementById('ijm-slide-date');
if (dateEl) dateEl.textContent = entry.formatted_date;
// Retroactive badge
const retroBadge = document.getElementById('ijm-slide-retroactive');
if (retroBadge) {
retroBadge.innerHTML = '';
if (entry.filled && entry.is_retroactive) {
retroBadge.innerHTML = `
<span class="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
</span>
`;
}
}
// Content area
const contentArea = document.getElementById('ijm-slide-content');
const badgeDiv = document.getElementById('ijm-slide-status-badge');
const actionBtn = document.getElementById('ijm-slide-action-btn');
if (!contentArea || !badgeDiv || !actionBtn) return;
if (!entry.filled) {
contentArea.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>
`;
badgeDiv.innerHTML = `<span class="text-xs text-slate-400 font-semibold">Boş Rapor</span>`;
actionBtn.classList.add('hidden');
} else {
actionBtn.classList.remove('hidden');
const entryContent = entry.content || '<p class="text-slate-400 italic">Boş içerik.</p>';
contentArea.innerHTML = `<div class="rich-text-content prose max-w-none text-slate-700 leading-relaxed text-sm select-text">${entryContent}</div>`;
updateDayApprovalUI(entry.supervisor_approved, entry.supervisor_name);
}
}
function updateDayApprovalUI(approved, name) {
const badgeDiv = document.getElementById('ijm-slide-status-badge');
const actionBtn = document.getElementById('ijm-slide-action-btn');
if (!badgeDiv || !actionBtn) return;
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 = "Günü 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 toggleActiveDayApproval() {
if (isUpdatingDayApproval) return;
const entry = journalEntries[activeSlideIndex];
if (!entry || !entry.entry_id) return;
const actionBtn = document.getElementById('ijm-slide-action-btn');
if (!actionBtn) return;
isUpdatingDayApproval = 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: entry.entry_id
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
// Update local object
entry.supervisor_approved = data.status;
entry.supervisor_name = data.supervisor_name;
// Update UI
updateDayApprovalUI(data.status, data.supervisor_name);
// Recalculate and update approved progress bar
let approvedDaysCount = 0;
journalEntries.forEach(e => {
if (e.filled && e.supervisor_approved) {
approvedDaysCount++;
}
});
const approvedPercent = journalEntries.length > 0 ? (approvedDaysCount / journalEntries.length) * 100 : 0;
const appTextEl = document.getElementById('ijm-approved-text');
if (appTextEl) appTextEl.textContent = `${approvedDaysCount} / ${journalEntries.length} Gün`;
const appBarEl = document.getElementById('ijm-approved-progress');
if (appBarEl) appBarEl.style.width = `${approvedPercent}%`;
} else {
alert(data.message || "Onay güncellenemedi.");
}
})
.catch(err => {
console.error(err);
alert("İşlem sırasında bir hata oluştu.");
})
.finally(() => {
isUpdatingDayApproval = false;
actionBtn.style.opacity = '1';
});
}
function prevSlide() {
if (activeSlideIndex > 0) {
renderSlide(activeSlideIndex - 1);
}
}
function nextSlide() {
if (activeSlideIndex < journalEntries.length - 1) {
renderSlide(activeSlideIndex + 1);
}
}
function goToSlide(idx) {
renderSlide(parseInt(idx));
}
// Admin Tab Switch Logic
document.addEventListener('DOMContentLoaded', function() {
const adminTabButtons = document.querySelectorAll('.admin-tab-btn');
const adminTabPanels = document.querySelectorAll('.admin-tab-panel');
adminTabButtons.forEach(button => {
button.addEventListener('click', () => {
// Deactivate all buttons
adminTabButtons.forEach(btn => {
btn.classList.remove('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
btn.classList.add('text-slate-600', 'bg-transparent', 'border-transparent');
btn.setAttribute('aria-selected', 'false');
});
// Hide all panels
adminTabPanels.forEach(panel => {
panel.classList.add('hidden');
});
// Activate clicked button
button.classList.add('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
button.classList.remove('text-slate-600', 'bg-transparent', 'border-transparent');
button.setAttribute('aria-selected', 'true');
// Show target panel
const targetId = button.getAttribute('data-tab-target');
const targetPanel = document.getElementById(targetId + '-panel');
if (targetPanel) {
targetPanel.classList.remove('hidden');
// Re-render DevExtreme Gantt chart if visible, to prevent rendering scale bugs
if (targetId === 'gantt-chart') {
const ganttEl = document.getElementById('gantt');
if (ganttEl) {
const ganttInstance = $(ganttEl).dxGantt('instance');
if (ganttInstance) {
ganttInstance.repaint();
}
}
}
}
});
});
});
</script>
@endpush
@endsection @endsection
@@ -3,7 +3,7 @@
@section('content') @section('content')
<section class="wrapper bg-[#f0f7ff] py-12"> <section class="wrapper bg-[#f0f7ff] py-12">
<div class="container px-4"> <div class="container px-4">
<div class="max-w-4xl mx-auto"> <div class="max-w-[1440px] mx-auto">
<!-- Top header / Welcome card --> <!-- Top header / Welcome card -->
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50 flex flex-col md:flex-row justify-between items-start md:items-center gap-6 mb-8"> <div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50 flex flex-col md:flex-row justify-between items-start md:items-center gap-6 mb-8">
@@ -655,15 +655,23 @@
<div class="p-5 rounded-2xl border border-slate-100 bg-slate-50/50 mb-6"> <div class="p-5 rounded-2xl border border-slate-100 bg-slate-50/50 mb-6">
<form action="{{ route('intern.save-github') }}" method="POST" class="space-y-4"> <form action="{{ route('intern.save-github') }}" method="POST" class="space-y-4">
@csrf @csrf
<div> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label for="github_repo" class="block text-xs font-extrabold text-slate-500 uppercase tracking-wider mb-2">GitHub Public Depo URL'si</label> <div>
<div class="flex flex-col sm:flex-row gap-3"> <label for="github_repo" class="block text-xs font-extrabold text-slate-500 uppercase tracking-wider mb-2">GitHub Public Depo URL'si</label>
<input type="url" name="github_repo" id="github_repo" placeholder="https://github.com/kullaniciadi/depo-adi" value="{{ $intern->github_repo }}" required 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" /> <input type="url" name="github_repo" id="github_repo" placeholder="https://github.com/kullaniciadi/depo-adi" value="{{ $intern->github_repo }}" required 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" />
<button type="submit" class="px-5 py-3 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-xl text-sm transition-all shadow-md shadow-blue-500/10 flex-shrink-0">
{{ $intern->github_repo ? 'Güncelle' : 'Kaydet' }}
</button>
</div> </div>
<span class="block text-xs text-slate-400 mt-2"><i class="uil uil-info-circle text-blue-600"></i> Deponuzun <strong>public (herkese açık)</strong> olması gerekmektedir.</span> <div>
<label for="github_username" class="block text-xs font-extrabold text-slate-500 uppercase tracking-wider mb-2">GitHub Kullanıcı Adınız (Opsiyonel)</label>
<input type="text" name="github_username" id="github_username" placeholder="GitHub kullanıcı adınız (örn: Emresatil)" value="{{ $intern->github_username }}" 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" />
</div>
</div>
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 pt-2 border-t border-slate-100">
<span class="text-xs text-slate-400 leading-normal max-w-xl">
<i class="uil uil-info-circle text-blue-600 text-sm"></i> Deponuzun <strong>public</strong> olması gerekir. Birden fazla kişi aynı projede çalışıyorsa, kendi commitlerinizi süzmek için kullanıcı adınızı yazmanız önerilir.
</span>
<button type="submit" class="px-5 py-3 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-xl text-sm transition-all shadow-md shadow-blue-500/10 flex-shrink-0 w-full sm:w-auto">
Bilgileri Kaydet
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -684,6 +692,7 @@
$dayDate = $d['date']; $dayDate = $d['date'];
$dayDateFormatted = $d['formatted_date']; $dayDateFormatted = $d['formatted_date'];
$hasSaved = isset($savedEntries[$dayNum]) && trim($savedEntries[$dayNum]->content) !== ''; $hasSaved = isset($savedEntries[$dayNum]) && trim($savedEntries[$dayNum]->content) !== '';
$isApproved = isset($savedEntries[$dayNum]) && $savedEntries[$dayNum]->supervisor_approved;
@endphp @endphp
<button type="button" <button type="button"
onclick="selectNotebookDay({{ $idx }})" onclick="selectNotebookDay({{ $idx }})"
@@ -692,13 +701,19 @@
data-date="{{ $dayDate }}" data-date="{{ $dayDate }}"
data-formatted-date="{{ $dayDateFormatted }}" data-formatted-date="{{ $dayDateFormatted }}"
data-saved-content="{{ $savedEntries[$dayNum]->content ?? '' }}" data-saved-content="{{ $savedEntries[$dayNum]->content ?? '' }}"
data-approved="{{ $isApproved ? '1' : '0' }}"
class="notebook-day-btn w-full flex items-center justify-between p-3 rounded-2xl border text-left transition-all @if($idx === 0) border-blue-200 bg-blue-50/50 @else border-slate-100 bg-white hover:bg-slate-50 @endif"> class="notebook-day-btn w-full flex items-center justify-between p-3 rounded-2xl border text-left transition-all @if($idx === 0) border-blue-200 bg-blue-50/50 @else border-slate-100 bg-white hover:bg-slate-50 @endif">
<div> <div>
<span class="block text-xs font-extrabold @if($idx === 0) text-blue-700 @else text-slate-800 @endif">{{ $dayNum }}. Gün</span> <span class="block text-xs font-extrabold @if($idx === 0) text-blue-700 @else text-slate-800 @endif">{{ $dayNum }}. Gün</span>
<span class="block text-[10px] text-slate-400 mt-0.5">{{ $dayDateFormatted }}</span> <span class="block text-[10px] text-slate-400 mt-0.5">{{ $dayDateFormatted }}</span>
</div> </div>
<span id="badge-day-{{ $idx }}" class="inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase @if($hasSaved) bg-green-50 text-green-700 border border-green-200 @else bg-slate-50 text-slate-400 border border-slate-200 @endif"> <span id="badge-day-{{ $idx }}" class="inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase
{{ $hasSaved ? 'DOLU' : 'BOŞ' }} @if($isApproved) bg-green-500 text-white border border-green-600
@elseif($hasSaved) bg-amber-50 text-amber-700 border border-amber-200
@else bg-slate-50 text-slate-400 border border-slate-200 @endif">
@if($isApproved) ONAYLANDI
@elseif($hasSaved) DOLU
@else BOŞ @endif
</span> </span>
</button> </button>
@endforeach @endforeach
@@ -708,28 +723,55 @@
<div class="lg:col-span-8 space-y-4"> <div class="lg:col-span-8 space-y-4">
<div class="bg-white border border-slate-100 rounded-2xl p-5 shadow-sm"> <div class="bg-white border border-slate-100 rounded-2xl p-5 shadow-sm">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 mb-4"> <div class="flex items-center justify-between pb-3 border-b border-slate-100 mb-4">
<div> <div class="flex items-center">
<span id="editor-day-title" class="text-sm font-extrabold text-blue-600 bg-blue-50 px-3 py-1 rounded-full">1. Gün</span> <span id="editor-day-title" class="text-sm font-extrabold text-blue-600 bg-blue-50 px-3 py-1 rounded-full">1. Gün</span>
<span id="editor-day-date" class="text-xs font-semibold text-slate-400 ml-2"></span> <span id="editor-day-date" class="text-xs font-semibold text-slate-400 ml-2"></span>
<span id="editor-day-status" class="ml-2"></span>
</div> </div>
@if($intern->github_repo) <div class="flex items-center gap-2">
<button type="button" onclick="fillFromGithub()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-900 text-white font-extrabold rounded-lg text-[10px] transition-all flex items-center gap-1.5 shadow-sm"> @if($intern->github_repo)
<i class="uil uil-github text-xs"></i> <button type="button" onclick="fillFromGithub()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-900 text-white font-extrabold rounded-lg text-[10px] transition-all flex items-center gap-1.5 shadow-sm">
<span>Git Commitlerini Çek</span> <i class="uil uil-github text-xs"></i>
</button> <span>Git Commitlerini Çek</span>
@endif </button>
@endif
<button type="button" onclick="printActiveDay()" class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white font-extrabold rounded-lg text-[10px] transition-all flex items-center gap-1.5 shadow-sm">
<i class="uil uil-print text-xs"></i>
<span>Yazdır</span>
</button>
</div>
</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 id="git-commits-fallback-container" class="hidden p-4 bg-slate-50 border border-slate-200/60 rounded-2xl text-xs space-y-3">
<div class="flex justify-between items-center pb-2 border-b border-slate-200">
<span class="font-extrabold text-slate-700 flex items-center gap-1.5">
<i class="uil uil-info-circle text-blue-600 text-sm"></i>
<span>Seçilen güne ait otomatik commit bulunamadı. Son 10 commit'iniz:</span>
</span>
<button type="button" onclick="hideFallbackCommits()" class="text-slate-400 hover:text-slate-600 font-extrabold">Kapat</button>
</div>
<div id="git-commits-fallback-list" class="space-y-2 max-h-[150px] overflow-y-auto pr-1 no-scrollbar">
<!-- items dynamically populated -->
</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>
@@ -788,11 +830,62 @@
</div> </div>
</div> </div>
<!-- Author Selection Modal -->
<div id="author-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
<div class="bg-white rounded-3xl p-6 max-w-md w-full shadow-2xl border border-slate-100/50 transform scale-95 opacity-0 transition-all duration-300" id="author-modal-card">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-bold text-slate-800 flex items-center gap-2">
<i class="uil uil-users-alt text-blue-600"></i>
<span>GitHub Yazar Seçimi</span>
</h3>
<p class="text-xs text-slate-400 mt-1">Bu günün commitlerinde birden fazla yazar tespit edildi. Hangisi sizsiniz?</p>
</div>
<button onclick="closeAuthorModal()" class="text-slate-400 hover:text-slate-600">
<i class="uil uil-multiply text-xl"></i>
</button>
</div>
<div id="author-list-container" class="space-y-2 mb-4 max-h-[220px] overflow-y-auto pr-1 no-scrollbar">
<!-- Dynamically populated author options -->
</div>
<div class="flex items-center gap-2 mb-5 bg-slate-50 p-3 rounded-xl border border-slate-200/60">
<input type="checkbox" id="save-selected-author-pref" class="rounded text-blue-600 focus:ring-blue-500 w-4 h-4" checked />
<label for="save-selected-author-pref" class="text-xs text-slate-600 font-bold select-none cursor-pointer">
Seçtiğim kullanıcı adını profilime kaydet ve varsayılan yap
</label>
</div>
<div class="flex justify-end gap-2">
<button type="button" onclick="closeAuthorModal()" class="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-600 font-bold rounded-xl text-xs transition-all">İptal</button>
</div>
</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>
let quill;
// Tab Switch Logic // Tab Switch Logic
document.addEventListener('DOMContentLoaded', function() { 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 tabButtons = document.querySelectorAll('.tab-btn');
const tabPanels = document.querySelectorAll('.tab-panel'); const tabPanels = document.querySelectorAll('.tab-panel');
@@ -831,49 +924,218 @@
// Handle initial state of date logic // Handle initial state of date logic
calculateEndDateFrontend(); calculateEndDateFrontend();
// Fetch Github commits in memory if repo is set
fetchAllCommitsInMemory();
// Select first day of the notebook workspace on load // Select first day of the notebook workspace on load
selectNotebookDay(0); selectNotebookDay(0);
}); });
const githubRepoUrl = @json($intern->github_repo); const githubRepoUrl = @json($intern->github_repo);
let allGithubCommits = {}; let githubUsername = @json($intern->github_username);
let activeDayIdx = 0; let activeDayIdx = 0;
function fetchAllCommitsInMemory() { let activeCommitsToProcess = [];
let activeCommitsDateVal = '';
function openAuthorModal(uniqueAuthors) {
const modal = document.getElementById('author-modal');
const card = document.getElementById('author-modal-card');
const container = document.getElementById('author-list-container');
if (!modal || !card || !container) return;
container.innerHTML = '';
// 1. Add "Hepsi" (All commits) option
const allBtn = document.createElement('button');
allBtn.type = 'button';
allBtn.className = "w-full p-3 rounded-xl border border-slate-200 hover:border-blue-400 hover:bg-blue-50/20 text-left transition-all font-extrabold text-xs text-slate-700 flex items-center justify-between";
allBtn.innerHTML = `
<span>Tüm Commitler (Filtreleme Yapma)</span>
<i class="uil uil-angle-right text-base text-slate-400"></i>
`;
allBtn.addEventListener('click', () => {
insertCommitsForAuthor(null);
});
container.appendChild(allBtn);
// 2. Add each unique author option
uniqueAuthors.forEach(author => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = "w-full p-3 rounded-xl border border-slate-200 hover:border-blue-400 hover:bg-blue-50/20 text-left transition-all font-extrabold text-xs text-slate-700 flex items-center justify-between mt-2";
btn.innerHTML = `
<span>${author}</span>
<i class="uil uil-angle-right text-base text-slate-400"></i>
`;
btn.addEventListener('click', () => {
insertCommitsForAuthor(author);
});
container.appendChild(btn);
});
modal.classList.remove('hidden');
setTimeout(() => {
card.classList.remove('scale-95', 'opacity-0');
card.classList.add('scale-100', 'opacity-100');
}, 10);
}
function closeAuthorModal() {
const modal = document.getElementById('author-modal');
const card = document.getElementById('author-modal-card');
if (!modal || !card) return;
card.classList.remove('scale-100', 'opacity-100');
card.classList.add('scale-95', 'opacity-0');
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
function insertCommitsForAuthor(author) {
closeAuthorModal();
const savePref = document.getElementById('save-selected-author-pref');
if (author && savePref && savePref.checked) {
saveGithubUsernamePreference(author);
}
let dayCommits = activeCommitsToProcess;
if (author) {
dayCommits = activeCommitsToProcess.filter(item => {
const commitAuthorName = item.commit.author.name;
const commitAuthorLogin = item.author ? item.author.login : null;
return (commitAuthorLogin && commitAuthorLogin.toLowerCase() === author.toLowerCase()) ||
(commitAuthorName && commitAuthorName.toLowerCase() === author.toLowerCase());
});
}
renderCommitsToEditor(dayCommits);
}
function saveGithubUsernamePreference(username) {
if (!githubRepoUrl) return; if (!githubRepoUrl) return;
let repoClean = githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, ''); fetch("{{ route('intern.save-github') }}", {
repoClean = repoClean.replace(/\/$/, ''); method: "POST",
const parts = repoClean.split('/'); headers: {
if (parts.length < 2) return; "Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
const owner = parts[0]; },
const repo = parts[1].replace(/\.git$/i, ''); body: JSON.stringify({
github_repo: githubRepoUrl,
fetch(`https://api.github.com/repos/${owner}/${repo}/commits?per_page=100`) github_username: username
.then(response => {
if (!response.ok) throw new Error('API Error');
return response.json();
}) })
.then(commits => { })
if (!Array.isArray(commits)) return; .then(response => response.json())
commits.forEach(item => { .then(data => {
const dateStr = item.commit.author.date; if (data.success) {
if (dateStr) { githubUsername = username;
const dateOnly = dateStr.substring(0, 10); const usernameInput = document.getElementById('github_username');
if (!allGithubCommits[dateOnly]) { if (usernameInput) {
allGithubCommits[dateOnly] = []; usernameInput.value = username;
} }
allGithubCommits[dateOnly].push(item); }
} })
}); .catch(err => {
}) console.warn("Kullanıcı adı tercihi kaydedilemedi.", err);
.catch(err => { });
console.warn('Commits could not be pre-loaded in memory.', err); }
function renderCommitsToEditor(commitsList) {
if (commitsList.length === 0) {
alert("Bu yazar için seçilen güne ait commit bulunamadı.");
return;
}
let commitHtml = "<ul>";
const sorted = [...commitsList].reverse();
sorted.forEach(c => {
const msg = c.commit.message;
const sha = c.sha.substring(0, 7);
const authorDate = new Date(c.commit.author.date);
const time = authorDate.toLocaleTimeString('tr-TR', { timeZone: 'Europe/Istanbul', hour: '2-digit', minute: '2-digit' });
commitHtml += `<li><strong>[${time}]</strong> (<em>${sha}</em>) ${msg}</li>`;
});
commitHtml += "</ul>";
const currentHtml = quill.root.innerHTML.trim();
const headerHtml = "<p><strong>Git Commit Mesajları:</strong></p>";
let newHtml = "";
if (currentHtml !== "" && currentHtml !== "<p><br></p>") {
newHtml = currentHtml + "<br><br>" + headerHtml + commitHtml;
} else {
newHtml = headerHtml + commitHtml;
}
quill.setContents([]);
quill.clipboard.dangerouslyPasteHTML(newHtml);
}
function hideFallbackCommits() {
const container = document.getElementById('git-commits-fallback-container');
if (container) {
container.classList.add('hidden');
}
}
function formatDateToIstanbul(d) {
try {
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'Europe/Istanbul',
year: 'numeric',
month: '2-digit',
day: '2-digit'
}); });
const parts = formatter.formatToParts(d);
const year = parts.find(p => p.type === 'year').value;
const month = parts.find(p => p.type === 'month').value;
const day = parts.find(p => p.type === 'day').value;
return `${year}-${month}-${day}`;
} catch (e) {
return d.toISOString().split('T')[0];
}
}
function updateEditorStatusBadge(isApproved, hasSaved) {
const statusSpan = document.getElementById('editor-day-status');
if (!statusSpan) return;
if (isApproved) {
statusSpan.textContent = 'ONAYLANDI';
statusSpan.className = 'inline-flex px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase bg-green-500 text-white border border-green-600 ml-2';
} else if (hasSaved) {
statusSpan.textContent = 'ONAY BEKLİYOR';
statusSpan.className = 'inline-flex px-2 py-0.5 rounded-full text-[10px] font-extrabold uppercase bg-amber-50 text-amber-700 border border-amber-200 ml-2';
} else {
statusSpan.textContent = '';
statusSpan.className = 'hidden';
}
}
function printActiveDay() {
const btn = document.getElementById(`btn-day-${activeDayIdx}`);
if (!btn) return;
const dayNum = btn.getAttribute('data-day-num');
window.open(`{{ route('intern.print-journal') }}?size=a4&day=${dayNum}`, '_blank');
}
function appendCommitToQuill(sha, time, msg) {
const escapedMsg = msg.replace(/</g, "&lt;").replace(/>/g, "&gt;");
const commitHtml = `<li><strong>[${time}]</strong> (<em>${sha}</em>) ${escapedMsg}</li>`;
const currentHtml = quill.root.innerHTML.trim();
const hasHeader = currentHtml.includes("Git Commit Mesajları:");
const headerHtml = hasHeader ? "" : "<p><strong>Git Commit Mesajları:</strong></p>";
let newHtml = "";
if (currentHtml !== "" && currentHtml !== "<p><br></p>") {
newHtml = currentHtml + "<br><br>" + headerHtml + "<ul>" + commitHtml + "</ul>";
} else {
newHtml = headerHtml + "<ul>" + commitHtml + "</ul>";
}
quill.setContents([]);
quill.clipboard.dangerouslyPasteHTML(newHtml);
} }
function selectNotebookDay(idx) { function selectNotebookDay(idx) {
@@ -903,11 +1165,51 @@
const dateVal = newBtn.getAttribute('data-date'); const dateVal = newBtn.getAttribute('data-date');
const dateFormatted = newBtn.getAttribute('data-formatted-date'); const dateFormatted = newBtn.getAttribute('data-formatted-date');
const savedContent = newBtn.getAttribute('data-saved-content'); const savedContent = newBtn.getAttribute('data-saved-content');
const isApproved = newBtn.getAttribute('data-approved') === '1';
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.setContents([]);
if (savedContent) {
quill.clipboard.dangerouslyPasteHTML(savedContent);
}
document.getElementById('save-status').textContent = ''; document.getElementById('save-status').textContent = '';
hideFallbackCommits();
const hasSaved = savedContent && savedContent.trim() !== '' && savedContent !== '<p><br></p>';
updateEditorStatusBadge(isApproved, hasSaved);
// 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');
}
}
} }
} }
@@ -916,33 +1218,166 @@
if (!btn) return; if (!btn) return;
const dateVal = btn.getAttribute('data-date'); const dateVal = btn.getAttribute('data-date');
const dayCommits = allGithubCommits[dateVal] || []; const formattedDate = btn.getAttribute('data-formatted-date');
if (dayCommits.length === 0) { if (!githubRepoUrl) {
alert("Bu staj gününe ait GitHub commit'i bulunamadı. Lütfen commit attığınızdan ve tarihin doğru olduğundan emin olun."); alert("Lütfen önce GitHub depo URL'nizi kaydedin.");
return; return;
} }
let commitText = ""; let repoClean = githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
const sorted = [...dayCommits].reverse(); repoClean = repoClean.replace(/\/$/, '');
sorted.forEach(c => { const parts = repoClean.split('/');
const msg = c.commit.message; if (parts.length < 2) {
const sha = c.sha.substring(0, 7); alert("Geçersiz GitHub URL'si.");
const authorDate = new Date(c.commit.author.date); return;
const time = authorDate.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
commitText += `- [${time}] (${sha}) ${msg}\n`;
});
const textarea = document.getElementById('editor-content');
if (textarea.value.trim() !== "") {
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();
} else {
textarea.value = commitText.trim();
}
} else {
textarea.value = commitText.trim();
} }
const owner = parts[0];
const repo = parts[1].replace(/\.git$/i, '');
let branch = '';
// Parse branch if /tree/{branch} or /commits/{branch} is used
if (parts.length >= 4 && (parts[2] === 'tree' || parts[2] === 'commits')) {
branch = parts.slice(3).join('/');
}
const gitBtn = document.querySelector('button[onclick="fillFromGithub()"]');
const originalContent = gitBtn.innerHTML;
gitBtn.disabled = true;
gitBtn.innerHTML = `<i class="uil uil-spinner text-xs animate-spin"></i> <span>Çekiliyor...</span>`;
hideFallbackCommits();
fetch(`https://api.github.com/repos/${owner}/${repo}/commits?per_page=100${branch ? `&sha=${branch}` : ''}`)
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error("Depo bulunamadı veya herkese açık (public) değil.");
} else if (response.status === 403) {
throw new Error("GitHub API istek limitine ulaşıldı. Lütfen daha sonra tekrar deneyin.");
} else {
throw new Error(`GitHub API Hatası (Kod: ${response.status})`);
}
}
return response.json();
})
.then(commits => {
if (!Array.isArray(commits)) {
throw new Error("Geçersiz API yanıtı.");
}
// Filter commits by the selected date
const dayCommits = commits.filter(item => {
const dateStr = item.commit.author.date;
if (!dateStr) return false;
const d = new Date(dateStr);
const dateOnly = formatDateToIstanbul(d);
return dateOnly === dateVal;
});
if (dayCommits.length === 0) {
// No commits found for this date. Show fallback list of latest 10 commits.
const fallbackContainer = document.getElementById('git-commits-fallback-container');
const fallbackList = document.getElementById('git-commits-fallback-list');
if (fallbackContainer && fallbackList) {
fallbackList.innerHTML = '';
const latestCommits = commits.slice(0, 10);
if (latestCommits.length === 0) {
fallbackList.innerHTML = `<p class="text-slate-400 italic">Depoda hiç commit bulunamadı.</p>`;
} else {
latestCommits.forEach(c => {
const msg = c.commit.message;
const sha = c.sha.substring(0, 7);
const authorDate = new Date(c.commit.author.date);
const time = authorDate.toLocaleTimeString('tr-TR', { timeZone: 'Europe/Istanbul', hour: '2-digit', minute: '2-digit' });
const commitDateOnly = formatDateToIstanbul(authorDate);
// format date to d.m.Y
const [cy, cm, cd] = commitDateOnly.split('-');
const formattedCommitDate = `${cd}.${cm}.${cy}`;
const row = document.createElement('div');
row.className = "flex items-center justify-between gap-3 p-2 hover:bg-slate-100 rounded-lg border border-slate-100 transition-colors";
const infoDiv = document.createElement('div');
infoDiv.className = "flex-grow min-w-0";
const timeSpan = document.createElement('span');
timeSpan.className = "font-bold text-slate-700 select-all";
timeSpan.textContent = `[${formattedCommitDate} ${time}] `;
const shaSpan = document.createElement('span');
shaSpan.className = "text-slate-500 font-semibold select-all";
shaSpan.textContent = `(${sha}) `;
const msgSpan = document.createElement('span');
msgSpan.className = "text-slate-600 truncate block sm:inline-block max-w-full font-medium ml-1";
msgSpan.textContent = msg;
infoDiv.appendChild(timeSpan);
infoDiv.appendChild(shaSpan);
infoDiv.appendChild(msgSpan);
const addBtn = document.createElement('button');
addBtn.type = 'button';
addBtn.className = "px-2 py-1 bg-blue-50 text-blue-600 hover:bg-blue-600 hover:text-white font-extrabold rounded transition-all flex-shrink-0 text-[10px]";
addBtn.textContent = "Ekle";
addBtn.addEventListener('click', () => {
appendCommitToQuill(sha, `${formattedCommitDate} ${time}`, msg);
});
row.appendChild(infoDiv);
row.appendChild(addBtn);
fallbackList.appendChild(row);
});
}
fallbackContainer.classList.remove('hidden');
alert(`Bu staj gününe (${formattedDate}) ait otomatik GitHub commit'i bulunamadı. Son 10 commit'iniz aşağıda listelenmiştir, rapora eklemek istediklerinizi seçebilirsiniz.`);
} else {
alert(`Bu staj gününe (${formattedDate}) ait GitHub commit'i bulunamadı. Lütfen commit attığınızdan ve tarihin doğru olduğundan emin olun.`);
}
return;
}
// Store these commits in memory for processing
activeCommitsToProcess = dayCommits;
activeCommitsDateVal = dateVal;
// Get unique authors
const authorsSet = new Set();
dayCommits.forEach(item => {
const login = item.author ? item.author.login : null;
const name = item.commit.author.name;
if (login) authorsSet.add(login);
if (name) authorsSet.add(name);
});
const uniqueAuthors = Array.from(authorsSet);
// If there are multiple authors
if (uniqueAuthors.length > 1) {
// If we have a saved githubUsername that matches one of the unique authors
const matchedAuthor = uniqueAuthors.find(a => githubUsername && a.toLowerCase() === githubUsername.toLowerCase());
if (matchedAuthor) {
// Automatically filter and insert without modal
insertCommitsForAuthor(matchedAuthor);
} else {
// Show modal to choose
openAuthorModal(uniqueAuthors);
}
} else {
// Only 1 author, insert directly
renderCommitsToEditor(dayCommits);
}
})
.catch(err => {
alert("Commitler çekilirken hata oluştu: " + err.message);
})
.finally(() => {
gitBtn.disabled = false;
gitBtn.innerHTML = originalContent;
});
} }
function saveActiveDay() { function saveActiveDay() {
@@ -951,7 +1386,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...";
@@ -976,17 +1414,21 @@
statusText.className = "text-xs font-semibold text-green-600"; statusText.className = "text-xs font-semibold text-green-600";
btn.setAttribute('data-saved-content', contentVal); btn.setAttribute('data-saved-content', contentVal);
btn.setAttribute('data-approved', '0');
const badge = document.getElementById(`badge-day-${activeDayIdx}`); const badge = document.getElementById(`badge-day-${activeDayIdx}`);
const hasContent = contentVal.trim() !== '' && contentVal !== '<p><br></p>';
if (badge) { if (badge) {
if (contentVal.trim() !== '') { if (hasContent) {
badge.textContent = 'DOLU'; badge.textContent = 'DOLU';
badge.className = "inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase bg-green-50 text-green-700 border border-green-200"; badge.className = "inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase bg-amber-50 text-amber-700 border border-amber-200";
} else { } else {
badge.textContent = 'BOŞ'; badge.textContent = 'BOŞ';
badge.className = "inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase bg-slate-50 text-slate-400 border border-slate-200"; badge.className = "inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase bg-slate-50 text-slate-400 border border-slate-200";
} }
} }
updateEditorStatusBadge(false, hasContent);
} else { } else {
statusText.textContent = data.message || "Kaydedilemedi."; statusText.textContent = data.message || "Kaydedilemedi.";
statusText.className = "text-xs font-semibold text-red-600"; statusText.className = "text-xs font-semibold text-red-600";
@@ -21,6 +21,40 @@
@endif @endif
</div> </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') @push('styles')
<!-- DevExtreme Gantt CSS Dependencies --> <!-- DevExtreme Gantt CSS Dependencies -->
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.5/css/dx.fluent.blue.light.css"> <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; border-left: 2px dashed #ef4444 !important;
width: 2px !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> </style>
<script> <script>
tailwind = { tailwind = {
@@ -62,6 +102,8 @@
progress: 0 progress: 0
})); }));
let dayColumnsMap = {};
$('#gantt').dxGantt({ $('#gantt').dxGantt({
tasks: { tasks: {
dataSource: formattedData, dataSource: formattedData,
@@ -105,8 +147,192 @@
start: new Date(), start: new Date(),
cssClass: 'current-time-line' 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> </script>
@endif @endif
@endpush @endpush
@@ -11,6 +11,7 @@
<tr class="border-b border-slate-100 text-xs font-extrabold uppercase tracking-wider text-slate-400"> <tr class="border-b border-slate-100 text-xs font-extrabold uppercase tracking-wider text-slate-400">
<th class="pb-4">Stajyer</th> <th class="pb-4">Stajyer</th>
<th class="pb-4">Staj Tarihleri</th> <th class="pb-4">Staj Tarihleri</th>
<th class="pb-4">Defter İlerlemesi</th>
<th class="pb-4">Durum</th> <th class="pb-4">Durum</th>
<th class="pb-4">Belgeler</th> <th class="pb-4">Belgeler</th>
<th class="pb-4">GitHub & Repo</th> <th class="pb-4">GitHub & Repo</th>
@@ -22,7 +23,10 @@
<tr class="hover:bg-slate-50/50 transition-colors"> <tr class="hover:bg-slate-50/50 transition-colors">
<!-- Name & Info --> <!-- Name & Info -->
<td class="py-4"> <td class="py-4">
<div class="font-bold text-slate-800 text-sm">{{ $intern->name }}</div> <div class="font-bold text-slate-800 text-sm cursor-pointer hover:underline hover:text-blue-600 flex items-center gap-1.5" onclick="openInternJournalModal({{ $intern->id }})">
<span>{{ $intern->name }}</span>
<span class="inline-flex px-1.5 py-0.5 rounded-md bg-blue-50 text-blue-600 text-[9px] font-extrabold uppercase border border-blue-100 tracking-wider">Defteri İncele</span>
</div>
<div class="text-xs text-slate-400 mt-0.5 flex flex-col gap-0.5"> <div class="text-xs text-slate-400 mt-0.5 flex flex-col gap-0.5">
<span><i class="uil uil-envelope mr-1"></i>{{ $intern->email }}</span> <span><i class="uil uil-envelope mr-1"></i>{{ $intern->email }}</span>
@if($intern->phone) @if($intern->phone)
@@ -43,6 +47,30 @@
@endif @endif
</td> </td>
<!-- Defter İlerlemesi -->
<td class="py-4">
@if($intern->internship_total_days)
@php
$filledDays = $intern->journalEntries->filter(function($entry) {
return !empty(trim($entry->content));
})->count();
$progressPercent = ($filledDays / $intern->internship_total_days) * 100;
if ($progressPercent > 100) $progressPercent = 100;
@endphp
<div class="flex flex-col gap-1 max-w-[130px]">
<div class="flex justify-between text-[10px] font-extrabold">
<span class="text-slate-400">Doldurulan</span>
<span class="text-blue-600">{{ $filledDays }} / {{ $intern->internship_total_days }} Gün</span>
</div>
<div class="w-full bg-slate-100 h-2 rounded-full overflow-hidden border border-slate-200/40">
<div class="bg-blue-600 h-full rounded-full transition-all duration-300" style="width: {{ $progressPercent }}%"></div>
</div>
</div>
@else
<span class="text-slate-300 italic text-xs">Belirlenmemiş</span>
@endif
</td>
<!-- Status --> <!-- Status -->
<td class="py-4"> <td class="py-4">
<span class="inline-flex px-2.5 py-1 rounded-full text-[10px] font-extrabold uppercase tracking-wider <span class="inline-flex px-2.5 py-1 rounded-full text-[10px] font-extrabold uppercase tracking-wider
+20 -23
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;
@@ -317,8 +331,8 @@
<span>Panele Dön</span> <span>Panele Dön</span>
</a> </a>
<div style="border-left: 1px solid #e2e8f0; margin: 0 5px; height: 35px;"></div> <div style="border-left: 1px solid #e2e8f0; margin: 0 5px; height: 35px;"></div>
<a href="?size=a4{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}" class="btn {{ $size === 'a4' ? 'btn-primary' : 'btn-secondary' }}">A4 Boyutu</a> <a href="?size=a4{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}{{ request()->has('day') ? '&day='.request('day') : '' }}" class="btn {{ $size === 'a4' ? 'btn-primary' : 'btn-secondary' }}">A4 Boyutu</a>
<a href="?size=a5{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}" class="btn {{ $size === 'a5' ? 'btn-primary' : 'btn-secondary' }}">A5 Boyutu</a> <a href="?size=a5{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}{{ request()->has('day') ? '&day='.request('day') : '' }}" class="btn {{ $size === 'a5' ? 'btn-primary' : 'btn-secondary' }}">A5 Boyutu</a>
<button onclick="window.print()" class="btn btn-primary"> <button onclick="window.print()" class="btn btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
<span>Yazdır / PDF Kaydet</span> <span>Yazdır / PDF Kaydet</span>
@@ -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
@@ -406,29 +420,12 @@
<!-- Supervisor --> <!-- Supervisor -->
<div class="signature-box"> <div class="signature-box">
<div class="sig-title">Staj Sorumlusu</div> <div class="sig-title">Staj Sorumlusu</div>
@if($intern->notebook_supervisor_signed) @if($entry && $entry->supervisor_approved)
<div class="sig-status"> <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> <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> <span class="sig-badge">E-ONAYLI</span>
</div> </div>
<div class="sig-name">{{ $intern->notebook_supervisor_name ?: 'Alperen Trunç' }}</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>
</div>
<div class="sig-name">&nbsp;</div>
@endif
</div>
<!-- Unit Officer -->
<div class="signature-box">
<div class="sig-title">Birim Yetkilisi</div>
@if($intern->notebook_unit_signed)
<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>
@else @else
<div class="sig-status waiting"> <div class="sig-status waiting">
<span class="sig-badge waiting">İMZA BEKLENİYOR</span> <span class="sig-badge waiting">İMZA BEKLENİYOR</span>
+106 -80
View File
@@ -3,6 +3,7 @@
@section('content') @section('content')
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700;800;900&family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,500;0,700;1,400&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700;800;900&family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,500;0,700;1,400&display=swap" rel="stylesheet">
@if($application->status === 'accepted' && $application->notebook_approved)
<div class="bg-slate-100 py-12 no-print"> <div class="bg-slate-100 py-12 no-print">
<div class="container max-w-6xl mx-auto px-4"> <div class="container max-w-6xl mx-auto px-4">
<!-- Verification Banner (e-Devlet Style) --> <!-- Verification Banner (e-Devlet Style) -->
@@ -21,88 +22,9 @@
<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>
<!-- ================= PAGE 3: STAJ DEFTERİ (PORTRAIT A4) ================= -->
@if($application->notebook_approved || $application->notebook_supervisor_signed || $application->notebook_unit_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">
<!-- Thin border decor for notebook -->
<div class="absolute inset-4 border border-slate-100 rounded-[1.5rem] pointer-events-none"></div>
<div class="relative z-10 flex flex-col justify-between h-full text-slate-700">
<div>
<!-- Header -->
<div class="flex justify-between items-center pb-2 border-b border-slate-100 mb-3">
<div>
<h2 class="font-serif font-bold text-slate-900 transcript-header-title">ONAYLI STAJ DEFTERİ RAPORU</h2>
<p class="text-slate-400 tracking-wider mt-0.5 transcript-header-subtitle">DİJİTAL İMZALI VE ONAYLI STAJ RAPORLARI</p>
</div>
<img src="{{ asset('logos/truncgil-yatay.svg') }}" alt="Trunçgil Logo" class="h-6 object-contain">
</div>
<!-- Signatures Information -->
<div class="bg-emerald-50 border border-emerald-200 rounded-xl p-3 mb-4 flex justify-between items-center">
<div>
<span class="text-xs font-extrabold text-emerald-800 uppercase block tracking-wider">Dijital İmza Durumu</span>
<span class="text-[10px] text-emerald-600 font-semibold block mt-0.5">Bu staj defteri yetkililer tarafından elektronik olarak imzalanmıştır.</span>
</div>
<div class="flex gap-2">
@if($application->notebook_supervisor_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">
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>
<!-- Notebook Days content overview -->
<div class="overflow-y-auto max-h-[700px] pr-2 no-scrollbar space-y-4">
@foreach($days as $day)
@php
$entry = $savedEntries->get($day['day_number']);
@endphp
@if($entry && trim($entry->content))
<div class="p-3 bg-slate-50 border border-slate-100 rounded-xl">
<div class="flex justify-between items-center pb-1.5 border-b border-slate-200/50 mb-1.5">
<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>
</div>
<p class="text-xs text-slate-600 whitespace-pre-wrap leading-relaxed" style="font-size: 11px;">{{ $entry->content }}</p>
</div>
@endif
@endforeach
</div>
</div>
<!-- Footer -->
<div class="w-full flex justify-between items-center pt-2 border-t border-slate-100 gap-4 mt-auto">
<div class="flex items-center gap-3">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data={{ urlencode(route('internship.verify', $application->certificate_code)) }}" alt="QR Code" class="w-14 h-14 border border-slate-200 p-0.5 rounded-lg bg-white">
<div>
<span class="font-bold text-slate-400 tracking-wider block verify-code-label">GÜVENLİ DOĞRULAMA KODU</span>
<span class="font-mono font-bold text-slate-800 block verify-code">{{ $application->certificate_code }}</span>
</div>
</div>
<div class="text-right text-slate-400 footer-address">
Trunçgil Teknoloji San. ve Tic. Ltd. Şti.<br>
Gaziantep Üniversitesi Teknopark No: 4A
</div>
</div>
</div>
</div>
</div>
@endif
</div> </div>
</div> </div>
@@ -146,7 +68,7 @@
<p> <p>
<strong>{{ \Carbon\Carbon::parse($application->internship_start_date)->format('d.m.Y') }}</strong> - <strong>{{ \Carbon\Carbon::parse($application->internship_start_date)->format('d.m.Y') }}</strong> -
<strong>{{ \Carbon\Carbon::parse($application->internship_end_date)->format('d.m.Y') }}</strong> tarihleri arasında, <strong>{{ \Carbon\Carbon::parse($application->internship_end_date)->format('d.m.Y') }}</strong> tarihleri arasında,
<strong>Trunçgil Teknoloji</strong> bünyesinde gerçekleştirdiği uzaktan staj programını başarıyla tamamlayarak bu belgeyi almaya hak kazanmıştır. <strong>Trunçgil Teknoloji</strong> bünyesinde gerçekleştirdiği staj programını başarıyla tamamlayarak bu belgeyi almaya hak kazanmıştır.
</p> </p>
<p class="text-xs md:text-sm text-slate-500 max-w-2xl mx-auto italic font-serif"> <p class="text-xs md:text-sm text-slate-500 max-w-2xl mx-auto italic font-serif">
Gösterdiği üstün gayret, sorumluluk bilinci, teknik beceriler ve ekip çalışmasına sağladığı değerli katkılardan dolayı teşekkür eder, profesyonel kariyerinde başarılar dileriz. Gösterdiği üstün gayret, sorumluluk bilinci, teknik beceriler ve ekip çalışmasına sağladığı değerli katkılardan dolayı teşekkür eder, profesyonel kariyerinde başarılar dileriz.
@@ -242,8 +164,97 @@
</div> </div>
</div> </div>
<!-- ================= PAGE 3: STAJ DEFTERİ (PORTRAIT A4) ================= -->
@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">
<!-- Thin border decor for notebook -->
<div class="absolute inset-4 border border-slate-100 rounded-[1.5rem] pointer-events-none"></div>
<div class="relative z-10 flex flex-col justify-between h-full text-slate-700">
<div>
<!-- Header -->
<div class="flex justify-between items-center pb-2 border-b border-slate-100 mb-3">
<div>
<h2 class="font-serif font-bold text-slate-900 transcript-header-title">ONAYLI STAJ DEFTERİ RAPORU</h2>
<p class="text-slate-400 tracking-wider mt-0.5 transcript-header-subtitle">DİJİTAL İMZALI VE ONAYLI STAJ RAPORLARI</p>
</div>
<img src="{{ asset('logos/truncgil-yatay.svg') }}" alt="Trunçgil Logo" class="h-6 object-contain">
</div>
<!-- Signatures Information -->
<div class="bg-emerald-50 border border-emerald-200 rounded-xl p-3 mb-4 flex justify-between items-center">
<div>
<span class="text-xs font-extrabold text-emerald-800 uppercase block tracking-wider">Dijital İmza Durumu</span>
<span class="text-[10px] text-emerald-600 font-semibold block mt-0.5">Bu staj defteri yetkililer tarafından elektronik olarak imzalanmıştır.</span>
</div>
<div class="flex gap-2">
@if($application->notebook_supervisor_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">
Sorumlu: {{ $application->notebook_supervisor_name ?: 'Alperen Trunç' }}
</span>
@endif
</div>
</div>
<!-- Notebook Days content overview -->
<div class="overflow-y-auto max-h-[700px] pr-2 no-scrollbar space-y-4">
@foreach($days as $day)
@php
$entry = $savedEntries->get($day['day_number']);
@endphp
@if($entry && trim($entry->content))
<div class="p-3 bg-slate-50 border border-slate-100 rounded-xl">
<div class="flex justify-between items-center pb-1.5 border-b border-slate-200/50 mb-1.5">
<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ı{{ $entry->supervisor_name ? ' (' . $entry->supervisor_name . ')' : '' }}</span>
@endif
</span>
<span class="text-[10px] text-slate-400 font-bold">{{ $day['formatted_date'] }}</span>
</div>
<div class="text-xs text-slate-600 leading-relaxed verify-log-content" style="font-size: 11px;">{!! $entry->content !!}</div>
</div>
@endif
@endforeach
</div>
</div>
<!-- Footer -->
<div class="w-full flex justify-between items-center pt-2 border-t border-slate-100 gap-4 mt-auto">
<div class="flex items-center gap-3">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data={{ urlencode(route('internship.verify', $application->certificate_code)) }}" alt="QR Code" class="w-14 h-14 border border-slate-200 p-0.5 rounded-lg bg-white">
<div>
<span class="font-bold text-slate-400 tracking-wider block verify-code-label">GÜVENLİ DOĞRULAMA KODU</span>
<span class="font-mono font-bold text-slate-800 block verify-code">{{ $application->certificate_code }}</span>
</div>
</div>
<div class="text-right text-slate-400 footer-address">
Trunçgil Teknoloji San. ve Tic. Ltd. Şti.<br>
Gaziantep Üniversitesi Teknopark No: 4A
</div>
</div>
</div>
</div>
</div>
@endif
</div> </div>
</div> </div>
@else
<div class="min-h-screen bg-slate-100 flex items-center justify-center p-6 no-print">
<div class="bg-white rounded-3xl p-8 max-w-md w-full shadow-2xl border border-slate-100 text-center">
<div class="w-16 h-16 rounded-full bg-red-50 text-[#e31e24] flex items-center justify-center text-3xl mx-auto mb-4">
<i class="uil uil-exclamation-triangle"></i>
</div>
<h1 class="text-xl font-bold text-slate-800">Doğrulanamayan Belge</h1>
<p class="text-xs text-slate-500 mt-2 leading-relaxed">Görüntülemeye çalıştığınız staj kaydı henüz onaylanmamıştır veya aktif değildir. Detaylı bilgi için kurum yetkilileri ile iletişime geçebilirsiniz.</p>
<a href="/" class="mt-6 inline-flex px-5 py-2.5 bg-slate-800 hover:bg-slate-900 text-white font-bold rounded-xl text-xs transition-all shadow-md">Anasayfaya Dön</a>
</div>
</div>
@endif
@push('styles') @push('styles')
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
@@ -254,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;
+20 -1
View File
@@ -17,16 +17,35 @@ $favicon_path = setting('site_favicon');
} }
@endphp @endphp
@php @php
$seo_canonical = $meta['canonical'] ?? url()->current(); $seo_canonical = $meta['canonical'] ?? null;
if (!$seo_canonical) {
$seo_canonical = url()->current();
$localeQuery = request()->query('locale') ?? request()->query('lang');
if ($localeQuery && function_exists('available_language_codes') && in_array($localeQuery, available_language_codes())) {
$seo_canonical .= '?locale=' . $localeQuery;
}
}
$seo_robots = $meta['robots'] ?? 'index, follow'; $seo_robots = $meta['robots'] ?? 'index, follow';
$seo_og_type = $meta['og_type'] ?? 'website'; $seo_og_type = $meta['og_type'] ?? 'website';
$seo_og_locale = str_replace('_', '-', $meta['locale'] ?? app()->getLocale()); $seo_og_locale = str_replace('_', '-', $meta['locale'] ?? app()->getLocale());
$activeLanguages = class_exists(\App\Models\Language::class)
? \App\Models\Language::where('is_active', true)->get()
: collect([]);
$currentUrl = url()->current();
@endphp @endphp
<title>{{ $seo_title }}</title> <title>{{ $seo_title }}</title>
<meta name="description" content="{{ $seo_description }}"> <meta name="description" content="{{ $seo_description }}">
<meta name="robots" content="{{ $seo_robots }}"> <meta name="robots" content="{{ $seo_robots }}">
<link rel="canonical" href="{{ $seo_canonical }}"> <link rel="canonical" href="{{ $seo_canonical }}">
@foreach($activeLanguages as $lang)
<link rel="alternate" hreflang="{{ $lang->code }}" href="{{ $currentUrl . '?locale=' . $lang->code }}">
@endforeach
@if($activeLanguages->count() > 0)
<link rel="alternate" hreflang="x-default" href="{{ $currentUrl }}">
@endif
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:type" content="{{ $seo_og_type }}"> <meta property="og:type" content="{{ $seo_og_type }}">
<meta property="og:url" content="{{ $seo_canonical }}"> <meta property="og:url" content="{{ $seo_canonical }}">
+7 -1
View File
@@ -1,8 +1,14 @@
{!! '<' . '?xml version="1.0" encoding="UTF-8"?' . '>' !!} {!! '<' . '?xml version="1.0" encoding="UTF-8"?' . '>' !!}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
@foreach($urls as $url) @foreach($urls as $url)
<url> <url>
<loc>{{ $url['loc'] }}</loc> <loc>{{ $url['loc'] }}</loc>
@if(!empty($url['alternates']))
@foreach($url['alternates'] as $alt)
<xhtml:link rel="alternate" hreflang="{{ $alt['hreflang'] }}" href="{{ $alt['href'] }}"/>
@endforeach
@endif
<lastmod>{{ $url['lastmod'] }}</lastmod> <lastmod>{{ $url['lastmod'] }}</lastmod>
<changefreq>{{ $url['changefreq'] }}</changefreq> <changefreq>{{ $url['changefreq'] }}</changefreq>
<priority>{{ $url['priority'] }}</priority> <priority>{{ $url['priority'] }}</priority>
+4
View File
@@ -131,6 +131,10 @@ 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::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::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/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::get('/stajyer/admin/journal-details', [\App\Http\Controllers\CareerController::class, 'getInternJournalDetails'])->name('intern.admin.get-journal-details');
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');
+383
View File
@@ -0,0 +1,383 @@
// Tab Switch Logic
document.addEventListener('DOMContentLoaded', function() {
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');
}
});
});
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
calculateEndDateFrontend();
// Fetch Github commits in memory if repo is set
fetchAllCommitsInMemory();
// Select first day of the notebook workspace on load
selectNotebookDay(0);
});
const githubRepoUrl = "dummy";
let allGithubCommits = {};
let activeDayIdx = 0;
function fetchAllCommitsInMemory() {
if (!githubRepoUrl) return;
let repoClean = githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
repoClean = repoClean.replace(/\/$/, '');
const parts = repoClean.split('/');
if (parts.length < 2) return;
const owner = parts[0];
const repo = parts[1].replace(/\.git$/i, '');
fetch(`https://api.github.com/repos/${owner}/${repo}/commits?per_page=100`)
.then(response => {
if (!response.ok) throw new Error('API Error');
return response.json();
})
.then(commits => {
if (!Array.isArray(commits)) return;
commits.forEach(item => {
const dateStr = item.commit.author.date;
if (dateStr) {
const dateOnly = dateStr.substring(0, 10);
if (!allGithubCommits[dateOnly]) {
allGithubCommits[dateOnly] = [];
}
allGithubCommits[dateOnly].push(item);
}
});
})
.catch(err => {
console.warn('Commits could not be pre-loaded in memory.', err);
});
}
function selectNotebookDay(idx) {
const oldBtn = document.getElementById(`btn-day-${activeDayIdx}`);
if (oldBtn) {
oldBtn.classList.remove('border-blue-200', 'bg-blue-50/50');
oldBtn.classList.add('border-slate-100', 'bg-white');
const oldTitle = oldBtn.querySelector('span.text-xs');
if (oldTitle) {
oldTitle.classList.remove('text-blue-700');
oldTitle.classList.add('text-slate-800');
}
}
activeDayIdx = idx;
const newBtn = document.getElementById(`btn-day-${idx}`);
if (newBtn) {
newBtn.classList.add('border-blue-200', 'bg-blue-50/50');
newBtn.classList.remove('border-slate-100', 'bg-white');
const newTitle = newBtn.querySelector('span.text-xs');
if (newTitle) {
newTitle.classList.add('text-blue-700');
newTitle.classList.remove('text-slate-800');
}
const dayNum = newBtn.getAttribute('data-day-num');
const dateVal = newBtn.getAttribute('data-date');
const dateFormatted = newBtn.getAttribute('data-formatted-date');
const savedContent = newBtn.getAttribute('data-saved-content');
document.getElementById('editor-day-title').textContent = `${dayNum}. Gün`;
document.getElementById('editor-day-date').textContent = dateFormatted;
quill.root.innerHTML = savedContent || '';
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');
}
}
}
}
function fillFromGithub() {
const btn = document.getElementById(`btn-day-${activeDayIdx}`);
if (!btn) return;
const dateVal = btn.getAttribute('data-date');
const dayCommits = allGithubCommits[dateVal] || [];
if (dayCommits.length === 0) {
alert("Bu staj gününe ait GitHub commit'i bulunamadı. Lütfen commit attığınızdan ve tarihin doğru olduğundan emin olun.");
return;
}
let commitHtml = "<ul>";
const sorted = [...dayCommits].reverse();
sorted.forEach(c => {
const msg = c.commit.message;
const sha = c.sha.substring(0, 7);
const authorDate = new Date(c.commit.author.date);
const time = authorDate.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
commitHtml += `<li><strong>[${time}]</strong> (<em>${sha}</em>) ${msg}</li>`;
});
commitHtml += "</ul>";
const currentHtml = quill.root.innerHTML.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.)")) {
quill.root.innerHTML = currentHtml + "<br><br>" + commitHtml;
} else {
quill.root.innerHTML = commitHtml;
}
} else {
quill.root.innerHTML = commitHtml;
}
}
function saveActiveDay() {
const btn = document.getElementById(`btn-day-${activeDayIdx}`);
if (!btn) return;
const dayNum = btn.getAttribute('data-day-num');
const dateVal = btn.getAttribute('data-date');
let contentVal = quill.root.innerHTML;
if (contentVal === '<p><br></p>') {
contentVal = '';
}
const statusText = document.getElementById('save-status');
statusText.textContent = "Kaydediliyor...";
statusText.className = "text-xs font-semibold text-slate-400 animate-pulse";
fetch(""dummy"", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": ""dummy""
},
body: JSON.stringify({
day_number: dayNum,
date: dateVal,
content: contentVal
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusText.textContent = "Başarıyla kaydedildi.";
statusText.className = "text-xs font-semibold text-green-600";
btn.setAttribute('data-saved-content', contentVal);
const badge = document.getElementById(`badge-day-${activeDayIdx}`);
if (badge) {
if (contentVal.trim() !== '') {
badge.textContent = 'DOLU';
badge.className = "inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase bg-green-50 text-green-700 border border-green-200";
} else {
badge.textContent = 'BOŞ';
badge.className = "inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase bg-slate-50 text-slate-400 border border-slate-200";
}
}
} else {
statusText.textContent = data.message || "Kaydedilemedi.";
statusText.className = "text-xs font-semibold text-red-600";
}
})
.catch(err => {
console.error(err);
statusText.textContent = "Bağlantı hatası oluştu.";
statusText.className = "text-xs font-semibold text-red-600";
});
}
function openPrintModal() {
const modal = document.getElementById('print-modal');
const card = document.getElementById('print-modal-card');
modal.classList.remove('hidden');
setTimeout(() => {
card.classList.remove('scale-95', 'opacity-0');
card.classList.add('scale-100', 'opacity-100');
}, 10);
}
function closePrintModal() {
const modal = document.getElementById('print-modal');
const card = document.getElementById('print-modal-card');
card.classList.remove('scale-100', 'opacity-100');
card.classList.add('scale-95', 'opacity-0');
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
function handleFileSelected(input) {
const fileNameElement = document.getElementById('selected-file-name');
const submitBtn = document.getElementById('submit-upload-btn');
if (input.files && input.files.length > 0) {
const file = input.files[0];
const fileSizeMB = file.size / (1024 * 1024);
if (fileSizeMB > 50) {
alert('Dosya boyutu 50MB\'ı aşamaz.');
input.value = '';
fileNameElement.textContent = '';
submitBtn.disabled = true;
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
return;
}
fileNameElement.textContent = 'Seçilen dosya: ' + file.name + ' (' + fileSizeMB.toFixed(2) + ' MB)';
submitBtn.disabled = false;
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
} else {
fileNameElement.textContent = '';
submitBtn.disabled = true;
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
}
function calculateEndDateFrontend() {
const startDateVal = document.getElementById('internship_start_date').value;
const totalDaysVal = document.getElementById('internship_total_days').value;
const endDateInput = document.getElementById('internship_end_date');
if (!startDateVal || !totalDaysVal || totalDaysVal <= 0) {
return;
}
let date = new Date(startDateVal);
let daysToAdd = parseInt(totalDaysVal);
let count = 0;
let endDate = new Date(startDateVal);
while (count < daysToAdd) {
const dayOfWeek = date.getDay();
if (dayOfWeek === 6 || dayOfWeek === 0) { // 6 = Saturday, 0 = Sunday
date.setDate(date.getDate() + 1);
continue;
}
endDate = new Date(date);
date.setDate(date.getDate() + 1);
count++;
}
// Format to YYYY-MM-DD
const yyyy = endDate.getFullYear();
const mm = String(endDate.getMonth() + 1).padStart(2, '0');
const dd = String(endDate.getDate()).padStart(2, '0');
endDateInput.value = `${yyyy}-${mm}-${dd}`;
}
function copyToClipboard(elementId, btn) {
const text = document.getElementById(elementId).textContent;
navigator.clipboard.writeText(text).then(() => {
const icon = btn.querySelector('i');
const originalClass = icon.className;
icon.className = 'uil uil-check text-green-500';
setTimeout(() => {
icon.className = originalClass;
}, 2000);
});
}