Files
citrus/app/Filament/Admin/Resources/InternApplications/InternApplicationResource.php
T

555 lines
33 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Filament\Admin\Resources\InternApplications;
use App\Models\CareerApplication;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Livewire;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Database\Eloquent\Builder;
class InternApplicationResource extends Resource
{
protected static ?string $model = CareerApplication::class;
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-academic-cap';
public static function getNavigationLabel(): string
{
return __('career.internship_title', ['default' => 'Staj Başvuruları']);
}
public static function getModelLabel(): string
{
return __('career.internship', ['default' => 'Staj Başvurusu']);
}
public static function getPluralModelLabel(): string
{
return __('career.internship_title', ['default' => 'Staj Başvuruları']);
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()->where('type', 'internship');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make('Tabs')
->tabs([
Tab::make('Kişisel ve Başvuru Bilgileri')
->icon('heroicon-m-user')
->schema([
TextInput::make('name')
->label(__('career.name'))
->required()
->disabled(),
TextInput::make('email')
->label(__('career.email'))
->email()
->required()
->disabled(),
TextInput::make('phone')
->label(__('career.phone'))
->disabled(),
Select::make('status')
->label(__('career.status'))
->options([
'pending' => __('career.pending'),
'reviewed' => __('career.reviewed'),
'rejected' => __('career.rejected'),
'accepted' => __('career.accepted'),
'waiting_document' => __('career.waiting_document'),
])
->required(),
Textarea::make('message')
->label(__('career.message'))
->disabled()
->columnSpanFull(),
])->columns(2),
Tab::make('Staj Belgeleri & Giriş Bilgileri')
->icon('heroicon-m-document-text')
->schema([
FileUpload::make('cv_path')
->label(__('career.cv'))
->disk('public')
->directory('cvs')
->required()
->disabled()
->downloadable(),
TextInput::make('username')
->label('Kullanıcı Adı')
->default(fn ($record) => $record?->email)
->disabled()
->dehydrated()
->autocomplete('new-username'),
TextInput::make('password')
->label('Şifre')
->password()
->revealable()
->autocomplete('new-password')
->formatStateUsing(fn () => null)
->dehydrateStateUsing(fn ($state) => filled($state) ? Hash::make($state) : null)
->dehydrated(fn ($state) => filled($state))
->placeholder('Şifreyi değiştirmek istemiyorsanız boş bırakın')
->nullable()
->suffixAction(
\Filament\Actions\Action::make('generatePassword')
->icon('heroicon-m-arrow-path')
->action(fn (Set $set) => $set('password', Str::random(12)))
),
FileUpload::make('to_be_signed_internship_form_path')
->label('İmzalanacak Staj Formu (Stajyerden)')
->disk('public')
->directory('to_be_signed_interns')
->downloadable()
->nullable(),
FileUpload::make('signed_internship_form_path')
->label('İmzalı Staj Formu')
->disk('public')
->directory('signed_interns')
->downloadable()
->live()
->afterStateUpdated(function ($state, Set $set) {
if ($state) {
$set('status', 'accepted');
}
})
->nullable(),
DatePicker::make('internship_start_date')
->label('Staj Başlangıç Tarihi')
->live()
->afterStateUpdated(function ($state, $get, Set $set) {
if ($state && $get('internship_total_days')) {
self::calculateEndDate($state, $get('internship_total_days'), $set);
} elseif ($state && $get('internship_end_date')) {
self::calculateTotalDays($state, $get('internship_end_date'), $set);
}
})
->nullable(),
DatePicker::make('internship_end_date')
->label('Staj Bitiş Tarihi')
->live()
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateTotalDays($get('internship_start_date'), $state, $set))
->nullable(),
TextInput::make('internship_total_days')
->label('Toplam Staj Süresi (İş Günü)')
->numeric()
->live()
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateEndDate($get('internship_start_date'), $state, $set))
->nullable(),
])->columns(2),
Tab::make('Staj Günlüğü')
->icon('heroicon-m-squares-plus')
->schema([
TextInput::make('github_repo')
->label('GitHub Depo URL\'si')
->url()
->nullable()
->live(),
Livewire::make(\App\Livewire\InternJournalTimeline::class)
->columnSpanFull()
]),
Tab::make('Staj Defteri & Onay')
->icon('heroicon-o-book-open')
->schema([
\Filament\Forms\Components\Placeholder::make('notebook_view')
->label('Doldurulan Staj Defteri')
->content(function ($record) {
if (!$record) return 'Henüz başvuru bulunmamaktadır.';
$days = \App\Http\Controllers\CareerController::getInternshipDates($record->internship_start_date, $record->internship_total_days);
if (empty($days)) return 'Staj başlangıç tarihi veya süresi girilmemiş.';
$saved = $record->journalEntries()->get()->keyBy('day_number');
$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) {
$dayNum = $d['day_number'];
$dateF = $d['formatted_date'];
$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="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>';
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>';
$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;
$unitApproved = $entry->unit_approved;
$html .= ' <div style="display:flex; align-items:center; gap:12px; margin-top:12px; padding-top:10px; border-top:1px dashed #e2e8f0; font-size:11px;">';
// Supervisor approval
$supBg = $supApproved ? '#d1fae5' : '#f1f5f9';
$supColor = $supApproved ? '#065f46' : '#64748b';
$supText = $supApproved ? 'Sorumlu Onayladı' : 'Sorumlu Onayı Bekliyor';
$html .= ' <span id="sup-badge-' . $entry->id . '" style="background:' . $supBg . '; color:' . $supColor . '; padding: 2px 8px; border-radius: 4px; font-weight: 700;">' . $supText . '</span>';
// Unit approval
$unitBg = $unitApproved ? '#d1fae5' : '#f1f5f9';
$unitColor = $unitApproved ? '#065f46' : '#64748b';
$unitText = $unitApproved ? 'Birim Yetkilisi Onayladı' : 'Birim Yetkilisi Onayı Bekliyor';
$html .= ' <span id="unit-badge-' . $entry->id . '" style="background:' . $unitBg . '; color:' . $unitColor . '; padding: 2px 8px; border-radius: 4px; font-weight: 700;">' . $unitText . '</span>';
// Buttons
$supBtnText = $supApproved ? 'Onayı Kaldır' : 'Onayla';
$supBtnBg = $supApproved ? '#ef4444' : '#2563eb';
$html .= ' <button type="button" onclick="toggleApproval(' . $entry->id . ', \'supervisor\', this)" style="margin-left:auto; background:' . $supBtnBg . '; color:white; border:none; padding:4px 10px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:10px;">Sorumlu ' . $supBtnText . '</button>';
$unitBtnText = $unitApproved ? 'Onayı Kaldır' : 'Onayla';
$unitBtnBg = $unitApproved ? '#ef4444' : '#2563eb';
$html .= ' <button type="button" onclick="toggleApproval(' . $entry->id . ', \'unit\', this)" style="background:' . $unitBtnBg . '; color:white; border:none; padding:4px 10px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:10px;">Yetkili ' . $unitBtnText . '</button>';
$html .= ' </div>';
}
$html .= '</div>';
}
$html .= '</div>';
// JS handler
$html .= '
<script>
if (typeof window.toggleApproval !== "function") {
window.toggleApproval = function(entryId, type, 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,
type: type
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (type === "supervisor") {
const badge = document.getElementById("sup-badge-" + entryId);
if (data.status) {
badge.style.background = "#d1fae5";
badge.style.color = "#065f46";
badge.textContent = "Sorumlu Onayladı";
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 {
const badge = document.getElementById("unit-badge-" + entryId);
if (data.status) {
badge.style.background = "#d1fae5";
badge.style.color = "#065f46";
badge.textContent = "Birim Yetkilisi Onayladı";
btn.textContent = "Yetkili Onayı Kaldır";
btn.style.background = "#ef4444";
} else {
badge.style.background = "#f1f5f9";
badge.style.color = "#64748b";
badge.textContent = "Birim Yetkilisi Onayı Bekliyor";
btn.textContent = "Yetkili Onayla";
btn.style.background = "#2563eb";
}
}
} else {
alert(data.message || "Bir hata oluştu.");
}
})
.catch(err => {
console.error(err);
alert("Bağlantı hatası oluştu.");
})
.finally(() => {
btn.disabled = false;
btn.style.opacity = "1";
});
};
}
</script>
';
// Add preview buttons
$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=a5&intern_id=' . $record->id . '" target="_blank" style="display:inline-flex; align-items:center; justify-content:center; padding: 8px 16px; background:#4b5563; color:white; border-radius:8px; font-weight:bold; font-size:12px; text-decoration:none; box-shadow: 0 1px 3px rgba(75, 85, 99, 0.2);">A5 Defteri Önizle / Yazdır</a>';
$html .= '</div>';
return new \Illuminate\Support\HtmlString($html);
})
->columnSpanFull(),
\Filament\Schemas\Components\Section::make('Onay ve İmza Bilgileri')
->schema([
\Filament\Forms\Components\Toggle::make('notebook_supervisor_signed')
->label('Staj Sorumlusu İmzala / Onayla')
->live(),
TextInput::make('notebook_supervisor_name')
->label('Staj Sorumlusu Adı / Ünvanı')
->placeholder('Örn: Alperen Trunç')
->default('Alperen Trunç'),
\Filament\Forms\Components\Toggle::make('notebook_unit_signed')
->label('İlgili Birim Yetkilisi İmzala / Onayla')
->live(),
TextInput::make('notebook_unit_name')
->label('Birim Yetkilisi Adı / Ünvanı')
->placeholder('Örn: Yetkili Birim Amiri')
->default('Yetkili Birim Amiri'),
\Filament\Forms\Components\Toggle::make('notebook_approved')
->label('Staj Defterini Genel Olarak Onayla')
->columnSpanFull(),
])->columns(2),
]),
Tab::make('Sertifika & Transkript')
->icon('heroicon-o-academic-cap')
->schema([
TextInput::make('certificate_code')
->label('Doğrulama Kodu')
->helperText('Belge kaydedildiğinde benzersiz doğrulama kodu otomatik olarak üretilir.')
->readonly()
->nullable(),
MarkdownEditor::make('transcript_markdown')
->label('Akademik Transkript (Markdown)')
->columnSpanFull()
->default(function () {
return "### STAJ AKADEMİK TRANSKRİPTİ VE PERFORMANS RAPORU\n\n" .
"#### 🛠️ Deneyimlenen Teknolojiler ve Kazanımlar\n" .
"| Modül / Çalışma Alanı | Kullanılan Teknolojiler / Araçlar | Değerlendirme |\n" .
"| --- | --- | --- |\n" .
"| Backend Mimari & API | Laravel framework, RESTful API, MySQL | Başarılı |\n" .
"| Arayüz & UI/UX Uygulamaları | Flutter, CSS, Glassmorphic Tasarım Prensipleri | Üstün Başarı |\n" .
"| Masaüstü & Sistem Entegrasyonu | Electron.js, Git / GitHub | Başarılı |\n" .
"| Takım Çalışması & Proje Yönetimi | Agile / Scrum, Slack, JIRA | Başarılı |\n\n" .
"#### 📊 Performans Değerlendirme Kriterleri\n" .
"| Değerlendirme Kriteri | Puan (100 Üzerinden) | Harf Notu |\n" .
"| --- | --- | --- |\n" .
"| Teknik Sorumluluk ve Görev Bilinci | 95 | AA |\n" .
"| Problem Çözme ve Analitik Düşünme | 90 | BA |\n" .
"| Ekip Çalışması ve İletişim Uyum | 95 | AA |\n" .
"| Öğrenme Hızı ve Adaptasyon | 100 | AA |\n" .
"| **GENEL BAŞARI ORTALAMASI** | **95.00** | **AA (Mükemmel)** |\n\n" .
"#### 📝 Danışman Görüşü ve Değerlendirme Notu\n" .
"\"Stajyerimiz, staj süresi boyunca kendisine verilen görevleri büyük bir titizlikle yerine getirmiştir. Özellikle karşılaştığı teknik problemlere getirdiği pratik çözümler ve yeni teknolojileri öğrenme isteği takdir edilmeye değerdir. Kurumumuz bünyesinde yürüttüğümüz projelere sağladığı katkılardan ötürü teşekkür eder, profesyonel kariyerinde başarılar dileriz.\"";
}),
])->columns(1)
])->columnSpanFull()
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('career.name'))
->searchable()
->sortable(),
TextColumn::make('email')
->label(__('career.email'))
->searchable()
->sortable(),
TextColumn::make('phone')
->label(__('career.phone'))
->searchable(),
TextColumn::make('status')
->label(__('career.status'))
->badge()
->color(fn (string $state): string => match ($state) {
'pending' => 'gray',
'reviewed' => 'info',
'rejected' => 'danger',
'accepted' => 'success',
'waiting_document' => 'warning',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
TextColumn::make('certificate_code')
->label('Sertifika Kodu')
->searchable()
->placeholder('Yok'),
TextColumn::make('created_at')
->label(__('career.created_at'))
->dateTime('d.m.Y H:i')
->sortable(),
])
->filters([
SelectFilter::make('status')
->label(__('career.status'))
->options([
'pending' => __('career.pending'),
'reviewed' => __('career.reviewed'),
'rejected' => __('career.rejected'),
'accepted' => __('career.accepted'),
'waiting_document' => __('career.waiting_document'),
]),
])
->actions([
Action::make('download_cv')
->label(__('career.download_cv'))
->icon('heroicon-o-arrow-down-tray')
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
->openUrlInNewTab(),
Action::make('download_signed_form')
->label('İmzalı Form İndir')
->icon('heroicon-o-document-check')
->url(fn ($record) => $record->signed_internship_form_path ? Storage::disk('public')->url($record->signed_internship_form_path) : null)
->visible(fn ($record) => !empty($record->signed_internship_form_path))
->openUrlInNewTab(),
Action::make('view_certificate')
->label('Sertifika Doğrulama')
->icon('heroicon-o-academic-cap')
->color('success')
->url(fn ($record) => $record->certificate_code ? route('internship.verify', $record->certificate_code) : null)
->visible(fn ($record) => !empty($record->certificate_code))
->openUrlInNewTab(),
DeleteAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('created_at', 'desc');
}
public static function calculateTotalDays($start, $end, Set $set): void
{
if (!$start || !$end) {
$set('internship_total_days', null);
return;
}
$startDate = \Carbon\Carbon::parse($start);
$endDate = \Carbon\Carbon::parse($end);
if ($startDate->gt($endDate)) {
$set('internship_total_days', 0);
return;
}
$days = 0;
while ($startDate->lte($endDate)) {
if (!$startDate->isWeekend()) {
$days++;
}
$startDate->addDay();
}
$set('internship_total_days', $days);
}
public static function calculateEndDate($start, $totalDays, Set $set): void
{
if (!$start || !$totalDays || $totalDays <= 0) {
return;
}
$startDate = \Carbon\Carbon::parse($start);
$daysToAdd = intval($totalDays);
$endDate = $startDate->copy();
$count = 0;
$temp = $startDate->copy();
while ($count < $daysToAdd) {
if ($temp->isWeekend()) {
$temp->addDay();
continue;
}
$endDate = $temp->copy();
$temp->addDay();
$count++;
}
$set('internship_end_date', $endDate->format('Y-m-d'));
}
public static function getPages(): array
{
return [
'index' => Pages\ListInternApplications::route('/'),
'create' => Pages\CreateInternApplication::route('/create'),
'edit' => Pages\EditInternApplication::route('/{record}/edit'),
];
}
public static function getWidgets(): array
{
return [
Widgets\InternGanttChartWidget::class,
];
}
}