feat: implement intern blog management system with approval workflows and administrative feedback

This commit is contained in:
Ümit Tunç
2026-07-29 16:18:21 +03:00
parent 7c04433f6d
commit aa003df568
8 changed files with 695 additions and 36 deletions
@@ -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')),
])
@@ -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('<div style="padding: 15px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; color: #64748b; font-size: 13px;">Bu stajyer henüz blog yazısı oluşturmadı.</div>');
}
$html = '<div style="space-y: 12px;">';
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 .= '<div style="padding: 14px; border: 1px solid #e2e8f0; border-radius: 10px; background: #ffffff; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">';
$html .= ' <div>';
$html .= ' <div style="display: flex; gap: 8px; align-items: center; margin-bottom: 4px;">';
$html .= ' <span style="font-size: 11px; font-weight: 800; background: #eff6ff; color: #1d4ed8; padding: 2px 8px; border-radius: 6px;">' . e($catLabel) . '</span>';
$html .= ' <span style="font-size: 11px; font-weight: 800; background: ' . $statusBg . '; color: ' . $statusColor . '; padding: 2px 8px; border-radius: 6px;">' . $statusText . '</span>';
$html .= ' </div>';
$html .= ' <h4 style="margin: 0; font-size: 14px; font-weight: bold; color: #1e293b;">' . e($b->title) . '</h4>';
if ($b->admin_feedback) {
$html .= ' <p style="margin: 4px 0 0 0; font-size: 11px; color: #dc2626;"><strong>Revizyon Notu:</strong> ' . e($b->admin_feedback) . '</p>';
}
$html .= ' </div>';
$html .= ' <div>';
if ($b->status === 'published') {
$html .= ' <a href="/blog/' . $b->slug . '" target="_blank" style="padding: 6px 12px; background: #059669; color: white; border-radius: 6px; font-size: 11px; font-weight: bold; text-decoration: none;">Sitede Gör</a>';
}
$html .= ' </div>';
$html .= '</div>';
}
$html .= '</div>';
return new \Illuminate\Support\HtmlString($html);
})
->columnSpanFull(),
])
])->columnSpanFull()
]);
}