diff --git a/app/Filament/Admin/Resources/Blogs/Tables/BlogsTable.php b/app/Filament/Admin/Resources/Blogs/Tables/BlogsTable.php index db9b75d..2ac371d 100644 --- a/app/Filament/Admin/Resources/Blogs/Tables/BlogsTable.php +++ b/app/Filament/Admin/Resources/Blogs/Tables/BlogsTable.php @@ -2,11 +2,14 @@ namespace App\Filament\Admin\Resources\Blogs\Tables; +use Filament\Actions\Action; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; use Filament\Actions\ForceDeleteBulkAction; use Filament\Actions\RestoreBulkAction; +use Filament\Forms\Components\Textarea; +use Filament\Notifications\Notification; use Filament\Tables\Columns\ImageColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\ToggleColumn; @@ -32,26 +35,45 @@ class BlogsTable ->sortable() ->limit(50), - TextColumn::make('slug') - ->label(__('blog.table_slug')) - ->searchable() - ->sortable() - ->limit(30), - TextColumn::make('status') ->label(__('blog.table_status')) ->badge() - ->color(fn (string $state): string => match ($state) { + ->color(fn (?string $state): string => match ($state) { 'draft' => 'gray', + 'pending' => 'info', 'published' => 'success', + 'rejected' => 'danger', 'archived' => 'warning', + default => 'gray', }) - ->formatStateUsing(fn (string $state): string => match ($state) { - 'draft' => __('blog.status_draft'), - 'published' => __('blog.status_published'), - 'archived' => __('blog.status_archived'), + ->formatStateUsing(fn (?string $state): string => match ($state) { + 'draft' => 'Taslak', + 'pending' => 'Onay Bekliyor', + 'published' => 'Yayınlandı', + 'rejected' => 'Revize İstendi', + 'archived' => 'Arşivlendi', + default => $state ?? '-', }), + TextColumn::make('careerApplication.name') + ->label('Stajyer') + ->searchable() + ->sortable() + ->badge() + ->color('purple') + ->toggleable(), + + TextColumn::make('intern_category') + ->label('Staj Konusu') + ->formatStateUsing(fn (?string $state): string => match ($state) { + 'experience' => '1. Staj Tecrübesi', + 'technical_challenge' => '2. Teknik Zorluklar', + 'product_showcase' => '3. Ürün Tanıtımı', + default => $state ?? '-', + }) + ->badge() + ->toggleable(), + TextColumn::make('author.name') ->label(__('blog.table_author')) ->searchable() @@ -75,39 +97,79 @@ class BlogsTable ToggleColumn::make('is_featured') ->label(__('blog.is_featured_field')) ->alignCenter(), - - TextColumn::make('created_at') - ->label(__('blog.table_created_at')) - ->dateTime('d.m.Y H:i') - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - - TextColumn::make('updated_at') - ->label(__('blog.table_updated_at')) - ->dateTime('d.m.Y H:i') - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ SelectFilter::make('status') ->label(__('blog.status_field')) ->options([ - 'draft' => __('blog.status_draft'), - 'published' => __('blog.status_published'), - 'archived' => __('blog.status_archived'), + 'draft' => 'Taslak', + 'pending' => 'Onay Bekliyor', + 'published' => 'Yayınlandı', + 'rejected' => 'Revize İstendi', + 'archived' => 'Arşivlendi', ]), + SelectFilter::make('intern_category') + ->label('Staj Blog Konusu') + ->options([ + 'experience' => '1. Staj Tecrübesi', + 'technical_challenge' => '2. Teknik Zorluklar', + 'product_showcase' => '3. Ürün Tanıtımı', + ]), + + TernaryFilter::make('is_intern_blog') + ->label('Sadece Stajyer Yazıları') + ->queries( + true: fn ($query) => $query->whereNotNull('career_application_id'), + false: fn ($query) => $query->whereNull('career_application_id'), + ), + SelectFilter::make('category_id') ->label(__('blog.category_field')) ->relationship('category', 'name'), - - TernaryFilter::make('is_featured') - ->label(__('blog.is_featured_field')), - - TernaryFilter::make('allow_comments') - ->label(__('blog.allow_comments_field')), ]) ->recordActions([ + Action::make('approve') + ->label('Onayla & Yayınla') + ->icon('heroicon-o-check-circle') + ->color('success') + ->visible(fn ($record) => in_array($record->status, ['pending', 'draft', 'rejected'])) + ->action(function ($record) { + $record->status = 'published'; + $record->published_at = now(); + $record->admin_feedback = null; + $record->save(); + + Notification::make() + ->title('Yazı Onaylandı') + ->body('Blog yazısı başarıyla yayınlandı.') + ->success() + ->send(); + }), + + Action::make('reject') + ->label('Revize İstə') + ->icon('heroicon-o-x-circle') + ->color('danger') + ->visible(fn ($record) => in_array($record->status, ['pending', 'published'])) + ->form([ + Textarea::make('admin_feedback') + ->label('Revizyon Gerekçesi / Stajyere Not') + ->required() + ->placeholder('Örn: Başlığı ve içerikteki kod bloklarını düzenleyiniz.'), + ]) + ->action(function ($record, array $data) { + $record->status = 'rejected'; + $record->admin_feedback = $data['admin_feedback']; + $record->save(); + + Notification::make() + ->title('Revizyon Talebi Gönderildi') + ->body('Stajyere revizyon bildirimi iletildi.') + ->warning() + ->send(); + }), + EditAction::make() ->label(__('blog.edit')), ]) diff --git a/app/Filament/Admin/Resources/InternApplications/InternApplicationResource.php b/app/Filament/Admin/Resources/InternApplications/InternApplicationResource.php index f523411..489b040 100644 --- a/app/Filament/Admin/Resources/InternApplications/InternApplicationResource.php +++ b/app/Filament/Admin/Resources/InternApplications/InternApplicationResource.php @@ -363,7 +363,71 @@ class InternApplicationResource extends Resource "#### 📝 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) + ])->columns(1), + + Tab::make('Stajyer Blog Yazıları') + ->icon('heroicon-o-newspaper') + ->schema([ + \Filament\Forms\Components\Placeholder::make('intern_blogs_view') + ->label('Yazılan Blog Yazıları') + ->content(function ($record) { + if (!$record) return 'Henüz kayıt bulunmuyor.'; + $blogs = $record->blogs()->orderBy('created_at', 'desc')->get(); + if ($blogs->isEmpty()) { + return new \Illuminate\Support\HtmlString('
Bu stajyer henüz blog yazısı oluşturmadı.
'); + } + + $html = '
'; + foreach ($blogs as $b) { + $catLabel = match ($b->intern_category) { + 'experience' => '1. Staj Tecrübesi', + 'technical_challenge' => '2. Teknik Zorluklar', + 'product_showcase' => '3. Ürün Tanıtımı', + default => $b->intern_category ?? '-', + }; + $statusBg = match ($b->status) { + 'published' => '#d1fae5', + 'pending' => '#fef3c7', + 'rejected' => '#fee2e2', + default => '#f1f5f9', + }; + $statusColor = match ($b->status) { + 'published' => '#065f46', + 'pending' => '#92400e', + 'rejected' => '#991b1b', + default => '#475569', + }; + $statusText = match ($b->status) { + 'published' => 'Yayınlandı', + 'pending' => 'Onay Bekliyor', + 'rejected' => 'Revize İstendi', + default => 'Taslak', + }; + + $html .= '
'; + $html .= '
'; + $html .= '
'; + $html .= ' ' . e($catLabel) . ''; + $html .= ' ' . $statusText . ''; + $html .= '
'; + $html .= '

' . e($b->title) . '

'; + if ($b->admin_feedback) { + $html .= '

Revizyon Notu: ' . e($b->admin_feedback) . '

'; + } + $html .= '
'; + $html .= '
'; + if ($b->status === 'published') { + $html .= ' Sitede Gör'; + } + $html .= '
'; + $html .= '
'; + } + $html .= '
'; + + return new \Illuminate\Support\HtmlString($html); + }) + ->columnSpanFull(), + ]) ])->columnSpanFull() ]); } diff --git a/app/Http/Controllers/CareerController.php b/app/Http/Controllers/CareerController.php index ddc8fa3..8ac9569 100644 --- a/app/Http/Controllers/CareerController.php +++ b/app/Http/Controllers/CareerController.php @@ -3,8 +3,10 @@ namespace App\Http\Controllers; use App\Models\CareerApplication; +use App\Models\Blog; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; class CareerController extends Controller { @@ -98,8 +100,11 @@ class CareerController extends Controller 'password' => 'required|string', ]); - $intern = CareerApplication::where('username', $request->username) - ->where('type', 'internship') + $intern = CareerApplication::where('type', 'internship') + ->where(function ($q) use ($request) { + $q->where('username', $request->username) + ->orWhere('email', $request->username); + }) ->first(); if (!$intern || !\Illuminate\Support\Facades\Hash::check($request->password, $intern->password)) { @@ -124,18 +129,109 @@ class CareerController extends Controller $intern = CareerApplication::findOrFail(session('intern_id')); $days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days); $savedEntries = $intern->journalEntries()->get()->keyBy('day_number'); + $blogs = $intern->blogs()->orderBy('created_at', 'desc')->get(); return view('front.career.intern_dashboard', [ 'intern' => $intern, 'days' => $days, 'savedEntries' => $savedEntries, + 'blogs' => $blogs, 'meta' => [ 'title' => 'Stajyer Paneli', - 'description' => 'Belgelerinizi buradan indirebilirsiniz.', + 'description' => 'Staj belgelerinizi ve blog yazılarınızı yönetin.', ] ]); } + public function saveInternBlog(Request $request) + { + if (!session()->has('intern_id')) { + return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.'); + } + + $intern = CareerApplication::findOrFail(session('intern_id')); + + $request->validate([ + 'blog_id' => 'nullable|integer|exists:blogs,id', + 'title' => 'required|string|max:255', + 'intern_category' => 'required|string|in:experience,technical_challenge,product_showcase', + 'excerpt' => 'nullable|string|max:500', + 'content' => 'required|string|min:50', + 'featured_image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:5120', + 'action_type' => 'required|string|in:draft,submit', + ], [ + 'title.required' => 'Lütfen blog yazısı başlığını girin.', + 'intern_category.required' => 'Lütfen bir kategori seçin.', + 'content.required' => 'Lütfen blog yazısı içeriğini girin.', + 'content.min' => 'Blog içeriği en az 50 karakter olmalıdır.', + 'featured_image.image' => 'Görsel geçerli bir resim dosyası olmalıdır.', + ]); + + $status = $request->action_type === 'draft' ? 'draft' : 'pending'; + + if ($request->filled('blog_id')) { + $blog = Blog::where('id', $request->blog_id) + ->where('career_application_id', $intern->id) + ->firstOrFail(); + } else { + $blog = new Blog(); + $blog->career_application_id = $intern->id; + } + + // Handle slug + if (!$blog->exists || $blog->title !== $request->title) { + $baseSlug = Str::slug($request->title); + $slug = $baseSlug; + $count = 1; + while (Blog::where('slug', $slug)->where('id', '!=', $blog->id ?? 0)->exists()) { + $slug = $baseSlug . '-' . $count; + $count++; + } + $blog->slug = $slug; + } + + $blog->title = $request->title; + $blog->intern_category = $request->intern_category; + $blog->excerpt = $request->excerpt; + $blog->content = $request->content; + $blog->status = $status; + $blog->meta_title = $request->title; + $blog->meta_description = Str::limit(strip_tags($request->excerpt ?: $request->content), 160); + + if ($request->hasFile('featured_image')) { + if ($blog->featured_image && Storage::disk('public')->exists($blog->featured_image)) { + Storage::disk('public')->delete($blog->featured_image); + } + $blog->featured_image = $request->file('featured_image')->store('blogs', 'public'); + } + + $blog->save(); + + $msg = $status === 'draft' ? 'Blog yazısı taslak olarak kaydedildi.' : 'Blog yazısı incelemeye gönderildi.'; + return redirect()->back()->with('success', $msg); + } + + public function deleteInternBlog($id) + { + if (!session()->has('intern_id')) { + return redirect()->route('intern.login')->with('error', 'Lütfen giriş yapın.'); + } + + $blog = Blog::where('id', $id) + ->where('career_application_id', session('intern_id')) + ->firstOrFail(); + + if (in_array($blog->status, ['draft', 'pending', 'rejected'])) { + if ($blog->featured_image && Storage::disk('public')->exists($blog->featured_image)) { + Storage::disk('public')->delete($blog->featured_image); + } + $blog->forceDelete(); + return redirect()->back()->with('success', 'Blog yazısı silindi.'); + } + + return redirect()->back()->with('error', 'Yayınlanmış blog yazıları silinemez.'); + } + public function uploadInternshipForm(Request $request) { if (!session()->has('intern_id')) { diff --git a/app/Models/Blog.php b/app/Models/Blog.php index fde3ee0..babd592 100644 --- a/app/Models/Blog.php +++ b/app/Models/Blog.php @@ -23,6 +23,9 @@ class Blog extends Model 'published_at', 'author_id', 'category_id', + 'career_application_id', + 'intern_category', + 'admin_feedback', 'tags', 'view_count', 'is_featured', @@ -53,6 +56,11 @@ class Blog extends Model return $this->belongsTo(User::class, 'author_id'); } + public function careerApplication() + { + return $this->belongsTo(CareerApplication::class, 'career_application_id'); + } + public function category() { return $this->belongsTo(BlogCategory::class, 'category_id'); diff --git a/app/Models/CareerApplication.php b/app/Models/CareerApplication.php index f975a49..8f00a65 100644 --- a/app/Models/CareerApplication.php +++ b/app/Models/CareerApplication.php @@ -48,6 +48,14 @@ class CareerApplication extends Model return $this->hasMany(InternshipJournalEntry::class); } + /** + * Get the blog posts written by this intern. + */ + public function blogs() + { + return $this->hasMany(Blog::class, 'career_application_id'); + } + protected static function booted() { static::saving(function ($model) { diff --git a/database/migrations/2026_07_29_141000_add_internship_fields_to_blogs_table.php b/database/migrations/2026_07_29_141000_add_internship_fields_to_blogs_table.php new file mode 100644 index 0000000..2ab80bc --- /dev/null +++ b/database/migrations/2026_07_29_141000_add_internship_fields_to_blogs_table.php @@ -0,0 +1,33 @@ +foreignId('career_application_id')->nullable()->after('author_id')->constrained('career_applications')->onDelete('cascade'); + $table->string('intern_category')->nullable()->after('career_application_id'); + $table->text('admin_feedback')->nullable()->after('intern_category'); + $table->foreignId('author_id')->nullable()->change(); + $table->string('status')->default('draft')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('blogs', function (Blueprint $table) { + $table->dropForeign(['career_application_id']); + $table->dropColumn(['career_application_id', 'intern_category', 'admin_feedback']); + }); + } +}; diff --git a/resources/views/front/career/intern_dashboard.blade.php b/resources/views/front/career/intern_dashboard.blade.php index cb5a53d..078743c 100644 --- a/resources/views/front/career/intern_dashboard.blade.php +++ b/resources/views/front/career/intern_dashboard.blade.php @@ -377,6 +377,23 @@ Günlük çalışma defteri + + + @php + $approvedBlogsCount = isset($blogs) ? $blogs->where('status', 'published')->count() : 0; + @endphp + @@ -785,6 +802,222 @@ + + + @@ -1563,6 +1796,89 @@ }, 2000); }); } + let blogQuill = null; + + document.addEventListener('DOMContentLoaded', function () { + if (document.getElementById('blog-quill-editor')) { + blogQuill = new Quill('#blog-quill-editor', { + theme: 'snow', + placeholder: 'Blog yazınızı buraya detaylı bir şekilde yazın...', + modules: { + toolbar: [ + [{ 'header': [1, 2, 3, false] }], + ['bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block'], + [{ 'list': 'ordered'}, { 'list': 'bullet' }], + ['link', 'clean'] + ] + } + }); + } + }); + + function openBlogModal() { + document.getElementById('modal_blog_id').value = ''; + document.getElementById('modal_blog_title').value = ''; + document.getElementById('modal_intern_category').value = 'experience'; + document.getElementById('modal_blog_excerpt').value = ''; + if (blogQuill) blogQuill.setContents([]); + document.getElementById('blog-modal-title').querySelector('span').textContent = 'Yeni Blog Yazısı Ekle'; + + const modal = document.getElementById('blog-modal'); + const card = document.getElementById('blog-modal-card'); + modal.classList.remove('hidden'); + setTimeout(() => { + card.classList.remove('scale-95', 'opacity-0'); + card.classList.add('scale-100', 'opacity-100'); + }, 10); + } + + function editBlogItem(blogItem) { + document.getElementById('modal_blog_id').value = blogItem.id; + document.getElementById('modal_blog_title').value = blogItem.title; + document.getElementById('modal_intern_category').value = blogItem.intern_category || 'experience'; + document.getElementById('modal_blog_excerpt').value = blogItem.excerpt || ''; + if (blogQuill && blogItem.content) { + blogQuill.setContents([]); + blogQuill.clipboard.dangerouslyPasteHTML(blogItem.content); + } + document.getElementById('blog-modal-title').querySelector('span').textContent = 'Blog Yazısını Düzenle'; + + const modal = document.getElementById('blog-modal'); + const card = document.getElementById('blog-modal-card'); + modal.classList.remove('hidden'); + setTimeout(() => { + card.classList.remove('scale-95', 'opacity-0'); + card.classList.add('scale-100', 'opacity-100'); + }, 10); + } + + function closeBlogModal() { + const modal = document.getElementById('blog-modal'); + const card = document.getElementById('blog-modal-card'); + card.classList.remove('scale-100', 'opacity-100'); + card.classList.add('scale-95', 'opacity-0'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); + } + + function submitBlogForm(actionType) { + const title = document.getElementById('modal_blog_title').value.trim(); + if (!title) { + alert('Lütfen blog başlığını giriniz.'); + return; + } + + const htmlContent = blogQuill ? blogQuill.root.innerHTML.trim() : ''; + if (!htmlContent || htmlContent === '


') { + alert('Lütfen blog içeriğini giriniz.'); + return; + } + + document.getElementById('modal_action_type').value = actionType; + document.getElementById('modal_blog_content').value = htmlContent; + document.getElementById('blog-form').submit(); + } @endpush + + + @include('front.career.partials.guide_modal') @endsection diff --git a/routes/web.php b/routes/web.php index 2bffd26..9fc5353 100644 --- a/routes/web.php +++ b/routes/web.php @@ -124,6 +124,8 @@ Route::post('/stajyer/github-kaydet', [\App\Http\Controllers\CareerController::c Route::get('/stajyer/gunluk-indir', [\App\Http\Controllers\CareerController::class, 'downloadMarkdown'])->name('intern.download-journal'); Route::post('/stajyer/defteri-kaydet', [\App\Http\Controllers\CareerController::class, 'saveJournalEntry'])->name('intern.save-journal'); Route::get('/stajyer/defteri-yazdir', [\App\Http\Controllers\CareerController::class, 'printJournal'])->name('intern.print-journal'); +Route::post('/stajyer/blog-kaydet', [\App\Http\Controllers\CareerController::class, 'saveInternBlog'])->name('intern.blog.save'); +Route::delete('/stajyer/blog/{id}/sil', [\App\Http\Controllers\CareerController::class, 'deleteInternBlog'])->name('intern.blog.delete'); Route::get('/staj-dogrulama/{code}', [\App\Http\Controllers\CareerController::class, 'verifyCertificate'])->name('internship.verify'); Route::post('/stajyer/cikis', [\App\Http\Controllers\CareerController::class, 'internLogout'])->name('intern.logout');