Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dd03cc9ca | |||
| dcda744b4a | |||
| cc750fcb1e | |||
| 483b0518fd | |||
| 26cab615f1 | |||
| 4576739e6e | |||
| aa003df568 | |||
| 7c04433f6d | |||
| 7d72ebc7ad | |||
| e172afb3ac |
@@ -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()
|
||||
]);
|
||||
}
|
||||
@@ -466,7 +530,7 @@ class InternApplicationResource extends Resource
|
||||
|
||||
$days = 0;
|
||||
while ($startDate->lte($endDate)) {
|
||||
if (!$startDate->isWeekend()) {
|
||||
if (!$startDate->isWeekend() && !\App\Helpers\TurkeyHolidayHelper::isHoliday($startDate)) {
|
||||
$days++;
|
||||
}
|
||||
$startDate->addDay();
|
||||
@@ -489,7 +553,7 @@ class InternApplicationResource extends Resource
|
||||
$temp = $startDate->copy();
|
||||
|
||||
while ($count < $daysToAdd) {
|
||||
if ($temp->isWeekend()) {
|
||||
if ($temp->isWeekend() || \App\Helpers\TurkeyHolidayHelper::isHoliday($temp)) {
|
||||
$temp->addDay();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Projects\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Projects\ProjectResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class CreateProject extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ProjectResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Yeni Proje Oluştur';
|
||||
}
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$this->record->recalculateProgress();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Projects\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Projects\ProjectResource;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class EditProject extends EditRecord
|
||||
{
|
||||
protected static string $resource = ProjectResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Proje Yönetimi: ' . $this->record->title;
|
||||
}
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('recalculate')
|
||||
->label('İlerlemeyi Yeniden Hesapla')
|
||||
->icon('heroicon-o-calculator')
|
||||
->color('info')
|
||||
->action(function () {
|
||||
$pct = $this->record->recalculateProgress();
|
||||
Notification::make()
|
||||
->title('Proje ilerleme yüzdesi güncellendi: %' . $pct)
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
|
||||
Action::make('preview_portal')
|
||||
->label('Müşteri Ekranında Gör')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('warning')
|
||||
->url(fn () => route('projects.show', $this->record->slug))
|
||||
->openUrlInNewTab(),
|
||||
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->record->recalculateProgress();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Projects\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Projects\ProjectResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListProjects extends ListRecords
|
||||
{
|
||||
protected static string $resource = ProjectResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Yeni Proje Başlat'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Projects;
|
||||
|
||||
use App\Filament\Admin\Resources\Projects\Pages\CreateProject;
|
||||
use App\Filament\Admin\Resources\Projects\Pages\EditProject;
|
||||
use App\Filament\Admin\Resources\Projects\Pages\ListProjects;
|
||||
use App\Filament\Admin\Resources\Projects\Schemas\ProjectForm;
|
||||
use App\Filament\Admin\Resources\Projects\Tables\ProjectsTable;
|
||||
use App\Models\Project;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class ProjectResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Project::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return 'Proje Takip';
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return 'Proje';
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return 'Proje Yönetimi';
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ProjectForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ProjectsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListProjects::route('/'),
|
||||
'create' => CreateProject::route('/create'),
|
||||
'edit' => EditProject::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRecordRouteBindingEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getRecordRouteBindingEloquentQuery()
|
||||
->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Projects\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ProjectForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Tabs::make('ProjectTabs')
|
||||
->tabs([
|
||||
Tab::make('Proje Detayları & Müşteri')
|
||||
->icon('heroicon-m-briefcase')
|
||||
->schema([
|
||||
Section::make('Temel Proje Bilgileri')
|
||||
->columns(12)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Proje Adı')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, $state, callable $set) {
|
||||
if ($operation !== 'create') return;
|
||||
$set('slug', Str::slug($state));
|
||||
})
|
||||
->columnSpan(6),
|
||||
|
||||
TextInput::make('slug')
|
||||
->label('URL Slug')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->rules(['alpha_dash'])
|
||||
->columnSpan(6),
|
||||
|
||||
TextInput::make('client_name')
|
||||
->label('Müşteri Unvanı / Firma')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpan(4),
|
||||
|
||||
TextInput::make('client_email')
|
||||
->label('Müşteri E-Posta')
|
||||
->email()
|
||||
->maxLength(255)
|
||||
->columnSpan(4),
|
||||
|
||||
TextInput::make('client_access_code')
|
||||
->label('Müşteri Giriş Şifresi / PIN')
|
||||
->default(fn () => strtoupper(Str::random(6)))
|
||||
->helperText('Müşterinin takip ekranına giriş yapabileceği şifre')
|
||||
->columnSpan(4),
|
||||
|
||||
Select::make('proposal_id')
|
||||
->label('İlişkili Fiyat Teklifi')
|
||||
->relationship('proposal', 'title')
|
||||
->searchable()
|
||||
->preload()
|
||||
->nullable()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, callable $set) {
|
||||
if ($state) {
|
||||
$prop = \App\Models\Proposal::find($state);
|
||||
if ($prop) {
|
||||
$set('slug', $prop->slug);
|
||||
}
|
||||
}
|
||||
})
|
||||
->columnSpan(4),
|
||||
|
||||
Select::make('status')
|
||||
->label('Proje Genel Durumu')
|
||||
->options([
|
||||
'planning' => 'Planlama Aşamasında',
|
||||
'in_progress' => 'Devam Ediyor (Aktif)',
|
||||
'on_hold' => 'Beklemeye Alındı',
|
||||
'completed' => 'Tamamlandı',
|
||||
'cancelled' => 'İptal Edildi',
|
||||
])
|
||||
->default('in_progress')
|
||||
->required()
|
||||
->columnSpan(4),
|
||||
|
||||
TextInput::make('progress_percent')
|
||||
->label('İlerleme Yüzdesi (%)')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->suffix('%')
|
||||
->columnSpan(4),
|
||||
|
||||
DatePicker::make('start_date')
|
||||
->label('Proje Başlangıç Tarihi')
|
||||
->native(false)
|
||||
->displayFormat('d.m.Y')
|
||||
->columnSpan(6),
|
||||
|
||||
DatePicker::make('target_date')
|
||||
->label('Hedef Bitiş Tarihi')
|
||||
->native(false)
|
||||
->displayFormat('d.m.Y')
|
||||
->columnSpan(6),
|
||||
|
||||
Textarea::make('notes')
|
||||
->label('İç Notlar ve Açıklamalar')
|
||||
->rows(3)
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make('Hizmet Modülleri (% İlerleme)')
|
||||
->icon('heroicon-m-squares-2x2')
|
||||
->schema([
|
||||
Section::make('Sözleşme Kapsamındaki Modüller')
|
||||
->description('Hangi modüllerin tamamlandığını işaretleyin. İlerleme yüzdesi modül ağırlıklarına göre otomatik hesaplanır.')
|
||||
->schema([
|
||||
Repeater::make('modules')
|
||||
->relationship('modules')
|
||||
->columns(12)
|
||||
->orderColumn('order')
|
||||
->defaultItems(0)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Modül Adı')
|
||||
->required()
|
||||
->columnSpan(4),
|
||||
|
||||
TextInput::make('weight_percent')
|
||||
->label('Ağırlık (%)')
|
||||
->numeric()
|
||||
->default(10)
|
||||
->suffix('%')
|
||||
->columnSpan(2),
|
||||
|
||||
Select::make('status')
|
||||
->label('Durum')
|
||||
->options([
|
||||
'pending' => 'Bekliyor',
|
||||
'in_progress' => 'Devam Ediyor',
|
||||
'completed' => 'Tamamlandı',
|
||||
])
|
||||
->default('pending')
|
||||
->required()
|
||||
->columnSpan(3),
|
||||
|
||||
DatePicker::make('start_date')
|
||||
->label('Başlangıç')
|
||||
->native(false)
|
||||
->columnSpan(1.5),
|
||||
|
||||
DatePicker::make('end_date')
|
||||
->label('Bitiş')
|
||||
->native(false)
|
||||
->columnSpan(1.5),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make('Kanban Görev Kartları')
|
||||
->icon('heroicon-m-view-columns')
|
||||
->schema([
|
||||
Section::make('İş Takvimi ve Görev Listesi')
|
||||
->description('Sözleşmedeki taskları ve detaylı görev kartlarını burada yönetin.')
|
||||
->schema([
|
||||
Repeater::make('tasks')
|
||||
->relationship('tasks')
|
||||
->columns(12)
|
||||
->orderColumn('order_index')
|
||||
->defaultItems(0)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Görev Başlığı')
|
||||
->required()
|
||||
->columnSpan(4),
|
||||
|
||||
Select::make('status')
|
||||
->label('Kanban Durumu')
|
||||
->options([
|
||||
'todo' => 'Yapılacak',
|
||||
'in_progress' => 'Devam Ediyor',
|
||||
'review' => 'Kontrol Bekliyor',
|
||||
'done' => 'Tamamlandı',
|
||||
])
|
||||
->default('todo')
|
||||
->required()
|
||||
->columnSpan(3),
|
||||
|
||||
Select::make('priority')
|
||||
->label('Öncelik')
|
||||
->options([
|
||||
'low' => 'Düşük',
|
||||
'medium' => 'Orta',
|
||||
'high' => 'Yüksek',
|
||||
'urgent' => 'Acil',
|
||||
])
|
||||
->default('medium')
|
||||
->columnSpan(2),
|
||||
|
||||
TextInput::make('assigned_person')
|
||||
->label('Sorumlu')
|
||||
->columnSpan(3),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make('Gidişat Güncellemeleri Logu')
|
||||
->icon('heroicon-m-chat-bubble-bottom-center-text')
|
||||
->schema([
|
||||
Section::make('Canlı İlerleme Duyuruları')
|
||||
->description('Yazılım ekibi tarafından girilen ve müşterinin canlı izleyebildiği durum mesajları.')
|
||||
->schema([
|
||||
Repeater::make('updates')
|
||||
->relationship('updates')
|
||||
->columns(12)
|
||||
->defaultItems(0)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label('Güncelleme Başlığı')
|
||||
->required()
|
||||
->columnSpan(6),
|
||||
|
||||
TextInput::make('progress_percent_at_update')
|
||||
->label('O Anki Yüzde (%)')
|
||||
->numeric()
|
||||
->columnSpan(3),
|
||||
|
||||
Checkbox::make('is_public')
|
||||
->label('Müşteriye Göster')
|
||||
->default(true)
|
||||
->columnSpan(3),
|
||||
|
||||
Textarea::make('content')
|
||||
->label('Güncelleme Notu / Sürüm Açıklaması')
|
||||
->required()
|
||||
->rows(2)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
])->columnSpanFull()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Projects\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ProjectsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label('Proje Adı')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(40),
|
||||
|
||||
TextColumn::make('client_name')
|
||||
->label('Müşteri')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(30),
|
||||
|
||||
TextColumn::make('progress_percent')
|
||||
->label('İlerleme')
|
||||
->formatStateUsing(fn ($state) => "%{$state}")
|
||||
->badge()
|
||||
->color(fn ($state) => match (true) {
|
||||
$state >= 100 => 'success',
|
||||
$state >= 50 => 'info',
|
||||
$state >= 25 => 'warning',
|
||||
default => 'danger',
|
||||
})
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label('Durum')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'planning' => 'gray',
|
||||
'in_progress' => 'info',
|
||||
'on_hold' => 'warning',
|
||||
'completed' => 'success',
|
||||
'cancelled' => 'danger',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'planning' => 'Planlama',
|
||||
'in_progress' => 'Devam Ediyor',
|
||||
'on_hold' => 'Beklemede',
|
||||
'completed' => 'Tamamlandı',
|
||||
'cancelled' => 'İptal',
|
||||
default => $state,
|
||||
}),
|
||||
|
||||
TextColumn::make('client_access_code')
|
||||
->label('Müşteri PIN')
|
||||
->copyable()
|
||||
->badge()
|
||||
->color('gray'),
|
||||
|
||||
TextColumn::make('target_date')
|
||||
->label('Hedef Bitiş')
|
||||
->date('d.m.Y')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label('Oluşturulma')
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->label('Durum')
|
||||
->options([
|
||||
'planning' => 'Planlama',
|
||||
'in_progress' => 'Devam Ediyor',
|
||||
'on_hold' => 'Beklemede',
|
||||
'completed' => 'Tamamlandı',
|
||||
'cancelled' => 'İptal',
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make()
|
||||
->label('Düzenle'),
|
||||
])
|
||||
->actions([
|
||||
Action::make('view_client_portal')
|
||||
->label('Müşteri Ekranı')
|
||||
->icon('heroicon-o-arrow-top-right-on-square')
|
||||
->url(fn ($record) => route('projects.show', $record->slug))
|
||||
->openUrlInNewTab(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->label('Sil'),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ class ProposalForm
|
||||
'coral' => 'Coral Corporate (Mercan Kırmızı)',
|
||||
'amber' => 'Amber Executive (Kehribar/Altın)',
|
||||
])
|
||||
->default('indigo')
|
||||
->default('coral')
|
||||
->live()
|
||||
->columnSpan(4),
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
class TurkeyHolidayHelper
|
||||
{
|
||||
/**
|
||||
* Checks if a given date is a national or religious holiday in Turkey.
|
||||
*
|
||||
* @param string|Carbon $date
|
||||
* @return bool
|
||||
*/
|
||||
public static function isHoliday($date): bool
|
||||
{
|
||||
$carbonDate = Carbon::parse($date);
|
||||
$md = $carbonDate->format('m-d'); // Month-Day
|
||||
$ymd = $carbonDate->format('Y-m-d'); // Year-Month-Day
|
||||
|
||||
// 1. Fixed Yearly Holidays
|
||||
$fixedHolidays = [
|
||||
'01-01', // Yılbaşı (New Year's Day)
|
||||
'04-23', // Ulusal Egemenlik ve Çocuk Bayramı
|
||||
'05-01', // Emek ve Dayanışma Günü
|
||||
'05-19', // Atatürk'ü Anma, Gençlik ve Spor Bayramı
|
||||
'07-15', // Demokrasi ve Milli Birlik Günü
|
||||
'08-30', // Zafer Bayramı
|
||||
'10-29', // Cumhuriyet Bayramı
|
||||
];
|
||||
|
||||
if (in_array($md, $fixedHolidays)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Variable Holidays (Ramazan & Kurban Bayramı including Eve/Arife days)
|
||||
// Hardcoded ranges for 2024 to 2030 based on Islamic lunar calendar projections.
|
||||
$variableHolidays = [
|
||||
// 2024
|
||||
'2024-04-09', // Ramazan Bayramı Arife
|
||||
'2024-04-10', '2024-04-11', '2024-04-12', // Ramazan Bayramı
|
||||
'2024-06-15', // Kurban Bayramı Arife
|
||||
'2024-06-16', '2024-06-17', '2024-06-18', '2024-06-19', // Kurban Bayramı
|
||||
|
||||
// 2025
|
||||
'2025-03-29', // Ramazan Bayramı Arife
|
||||
'2025-03-30', '2025-03-31', '2025-04-01', // Ramazan Bayramı
|
||||
'2025-06-05', // Kurban Bayramı Arife
|
||||
'2025-06-06', '2025-06-07', '2025-06-08', '2025-06-09', // Kurban Bayramı
|
||||
|
||||
// 2026
|
||||
'2026-03-19', // Ramazan Bayramı Arife
|
||||
'2026-03-20', '2026-03-21', '2026-03-22', // Ramazan Bayramı
|
||||
'2026-05-26', // Kurban Bayramı Arife
|
||||
'2026-05-27', '2026-05-28', '2026-05-29', '2026-05-30', // Kurban Bayramı
|
||||
|
||||
// 2027
|
||||
'2027-03-08', // Ramazan Bayramı Arife
|
||||
'2027-03-09', '2027-03-10', '2027-03-11', // Ramazan Bayramı
|
||||
'2027-05-15', // Kurban Bayramı Arife
|
||||
'2027-05-16', '2027-05-17', '2027-05-18', '2027-05-19', // Kurban Bayramı
|
||||
|
||||
// 2028
|
||||
'2028-02-26', // Ramazan Bayramı Arife
|
||||
'2028-02-27', '2028-02-28', '2028-02-29', // Ramazan Bayramı
|
||||
'2028-05-04', // Kurban Bayramı Arife
|
||||
'2028-05-05', '2028-05-06', '2028-05-07', '2028-05-08', // Kurban Bayramı
|
||||
|
||||
// 2029
|
||||
'2029-02-14', // Ramazan Bayramı Arife
|
||||
'2029-02-15', '2029-02-16', '2029-02-17', // Ramazan Bayramı
|
||||
'2029-04-23', // Kurban Bayramı Arife (Note: also April 23)
|
||||
'2029-04-24', '2029-04-25', '2029-04-26', '2029-04-27', // Kurban Bayramı
|
||||
|
||||
// 2030
|
||||
'2030-02-03', // Ramazan Bayramı Arife
|
||||
'2030-02-04', '2030-02-05', '2030-02-06', // Ramazan Bayramı
|
||||
'2030-04-12', // Kurban Bayramı Arife
|
||||
'2030-04-13', '2030-04-14', '2030-04-15', '2030-04-16', // Kurban Bayramı
|
||||
];
|
||||
|
||||
return in_array($ymd, $variableHolidays);
|
||||
}
|
||||
}
|
||||
@@ -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')) {
|
||||
@@ -177,7 +273,7 @@ class CareerController extends Controller
|
||||
$temp = $startDate->copy();
|
||||
|
||||
while ($count < $daysToAdd) {
|
||||
if ($temp->isWeekend()) {
|
||||
if ($temp->isWeekend() || \App\Helpers\TurkeyHolidayHelper::isHoliday($temp)) {
|
||||
$temp->addDay();
|
||||
continue;
|
||||
}
|
||||
@@ -427,9 +523,18 @@ class CareerController extends Controller
|
||||
];
|
||||
});
|
||||
|
||||
$unapprovedCount = \App\Models\InternshipJournalEntry::where('supervisor_approved', false)
|
||||
->whereNotNull('content')
|
||||
->where('content', '!=', '')
|
||||
->whereHas('careerApplication', function ($q) {
|
||||
$q->where('type', 'internship');
|
||||
})
|
||||
->count();
|
||||
|
||||
return view('front.career.admin_dashboard', [
|
||||
'allInterns' => $allInterns,
|
||||
'ganttInterns' => $ganttInterns,
|
||||
'unapprovedCount' => $unapprovedCount,
|
||||
'meta' => [
|
||||
'title' => 'Staj Yönetici Paneli',
|
||||
'description' => 'Stajyer bilgilerini ve takvimlerini izleyin.',
|
||||
@@ -455,7 +560,7 @@ class CareerController extends Controller
|
||||
$count = 0;
|
||||
|
||||
while ($count < $daysToAdd) {
|
||||
if ($temp->isWeekend()) {
|
||||
if ($temp->isWeekend() || \App\Helpers\TurkeyHolidayHelper::isHoliday($temp)) {
|
||||
$temp->addDay();
|
||||
continue;
|
||||
}
|
||||
@@ -492,19 +597,34 @@ class CareerController extends Controller
|
||||
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()) {
|
||||
return response()->json(['success' => false, 'message' => 'Hafta sonu günlerine staj günlüğü girilemez.'], 422);
|
||||
// 2. Prevent weekend and holiday entries
|
||||
if ($dateObj->isWeekend() || \App\Helpers\TurkeyHolidayHelper::isHoliday($dateObj)) {
|
||||
return response()->json(['success' => false, 'message' => 'Hafta sonu günlerine ve resmi tatillere staj günlüğü girilemez.'], 422);
|
||||
}
|
||||
|
||||
// 3. Determine if retroactive
|
||||
// 3. Verify alignment of day_number and date based on internship calendar
|
||||
$dates = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
|
||||
$dateToDayMap = [];
|
||||
foreach ($dates as $d) {
|
||||
$dateToDayMap[$d['date']] = $d['day_number'];
|
||||
}
|
||||
|
||||
$expectedDay = $dateToDayMap[$request->date] ?? null;
|
||||
if ($expectedDay === null || $expectedDay != $request->day_number) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Seçilen tarih ve gün sırası uyuşmuyor. Lütfen sayfayı yenileyip tekrar deneyin.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 4. Determine if retroactive
|
||||
$isRetroactive = $dateObj->lt(\Carbon\Carbon::today());
|
||||
|
||||
$entry = \App\Models\InternshipJournalEntry::updateOrCreate([
|
||||
'career_application_id' => $intern->id,
|
||||
'day_number' => $request->day_number,
|
||||
'date' => $request->date,
|
||||
], [
|
||||
'date' => $request->date,
|
||||
'content' => $request->content,
|
||||
'is_retroactive' => $isRetroactive,
|
||||
'supervisor_approved' => false,
|
||||
@@ -740,5 +860,53 @@ class CareerController extends Controller
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function getQuickApprovalEntries(Request $request)
|
||||
{
|
||||
if (!auth()->check() || !auth()->user()->hasRole('super_admin')) {
|
||||
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
|
||||
}
|
||||
|
||||
$type = $request->query('type', 'unapproved'); // 'unapproved' or 'today'
|
||||
$date = $request->query('date');
|
||||
|
||||
$query = \App\Models\InternshipJournalEntry::with('careerApplication')
|
||||
->whereHas('careerApplication', function ($q) {
|
||||
$q->where('type', 'internship');
|
||||
});
|
||||
|
||||
if ($type === 'today') {
|
||||
$targetDate = $date ?: \Carbon\Carbon::today()->format('Y-m-d');
|
||||
$query->whereDate('date', $targetDate);
|
||||
} elseif ($type === 'unapproved') {
|
||||
$query->where('supervisor_approved', false);
|
||||
}
|
||||
|
||||
// Only return entries that have content
|
||||
$query->whereNotNull('content')->where('content', '!=', '');
|
||||
|
||||
$entries = $query->orderBy('date', 'desc')->get()->map(function ($entry) {
|
||||
return [
|
||||
'id' => $entry->id,
|
||||
'day_number' => $entry->day_number,
|
||||
'date' => $entry->date,
|
||||
'formatted_date' => \Carbon\Carbon::parse($entry->date)->format('d.m.Y'),
|
||||
'content' => $entry->content,
|
||||
'is_retroactive' => (bool)$entry->is_retroactive,
|
||||
'supervisor_approved' => (bool)$entry->supervisor_approved,
|
||||
'supervisor_name' => $entry->supervisor_name,
|
||||
'intern' => [
|
||||
'id' => $entry->careerApplication->id,
|
||||
'name' => $entry->careerApplication->name,
|
||||
'email' => $entry->careerApplication->email,
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'entries' => $entries
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectModule;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\ProjectUpdate;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ProjectController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the public/client project tracking portal & manager view.
|
||||
*/
|
||||
public function show(Request $request, string $slug)
|
||||
{
|
||||
$project = Project::where('slug', $slug)
|
||||
->with(['modules', 'tasks', 'updates' => function($q) {
|
||||
$q->latest();
|
||||
}, 'proposal'])
|
||||
->firstOrFail();
|
||||
|
||||
// Recalculate progress dynamically
|
||||
$project->recalculateProgress();
|
||||
|
||||
// Strict Admin Check: ONLY logged-in admin users can edit/manage
|
||||
$isAdminMode = Auth::check();
|
||||
|
||||
// Client PIN Verification Check: Admins are auto-verified, clients need PIN verification in session
|
||||
$isVerified = $isAdminMode || session()->get('project_access_' . $project->id, false);
|
||||
|
||||
return view('projects.show', compact('project', 'isAdminMode', 'isVerified'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify client access code (PIN).
|
||||
*/
|
||||
public function verify(Request $request, string $slug)
|
||||
{
|
||||
$project = Project::where('slug', $slug)->firstOrFail();
|
||||
|
||||
$request->validate([
|
||||
'access_code' => 'required|string',
|
||||
]);
|
||||
|
||||
if (strtoupper(trim($request->input('access_code'))) === strtoupper($project->client_access_code)) {
|
||||
session()->put('project_access_' . $project->id, true);
|
||||
return back()->with('success', 'Erişim doğrulandı.');
|
||||
}
|
||||
|
||||
return back()->withErrors(['access_code' => 'Geçersiz müşteri takip şifresi.']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Manager Action: Update Module Status (Bekliyor / Devam Ediyor / Tamamlandı)
|
||||
*/
|
||||
public function updateModuleStatus(Request $request, string $slug)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json(['success' => false, 'message' => 'Bu işlem için yönetici girişi yapmanız gerekmektedir.'], 403);
|
||||
}
|
||||
return back()->withErrors(['error' => 'Yetkisiz erişim.']);
|
||||
}
|
||||
|
||||
$project = Project::where('slug', $slug)->firstOrFail();
|
||||
|
||||
$request->validate([
|
||||
'module_id' => 'required|exists:project_modules,id',
|
||||
'status' => 'required|in:pending,in_progress,completed',
|
||||
]);
|
||||
|
||||
$module = ProjectModule::where('id', $request->input('module_id'))
|
||||
->where('project_id', $project->id)
|
||||
->firstOrFail();
|
||||
|
||||
$module->update(['status' => $request->input('status')]);
|
||||
$newProgress = $project->recalculateProgress();
|
||||
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "'{$module->title}' modül durumu güncellendi.",
|
||||
'progress_percent' => $newProgress,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', "'{$module->title}' modül durumu güncellendi.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Manager Action: Update Task Status (Kanban Move)
|
||||
*/
|
||||
public function updateTaskStatus(Request $request, string $slug)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json(['success' => false, 'message' => 'Bu işlem için yönetici girişi yapmanız gerekmektedir.'], 403);
|
||||
}
|
||||
return back()->withErrors(['error' => 'Yetkisiz erişim.']);
|
||||
}
|
||||
|
||||
$project = Project::where('slug', $slug)->firstOrFail();
|
||||
|
||||
$request->validate([
|
||||
'task_id' => 'required|exists:project_tasks,id',
|
||||
'status' => 'required|in:todo,in_progress,review,done',
|
||||
]);
|
||||
|
||||
$task = ProjectTask::where('id', $request->input('task_id'))
|
||||
->where('project_id', $project->id)
|
||||
->firstOrFail();
|
||||
|
||||
$task->update(['status' => $request->input('status')]);
|
||||
$newProgress = $project->recalculateProgress();
|
||||
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "'{$task->title}' görev durumu güncellendi.",
|
||||
'progress_percent' => $newProgress,
|
||||
'counts' => [
|
||||
'todo' => $project->tasks()->where('status', 'todo')->count(),
|
||||
'in_progress' => $project->tasks()->where('status', 'in_progress')->count(),
|
||||
'review' => $project->tasks()->where('status', 'review')->count(),
|
||||
'done' => $project->tasks()->where('status', 'done')->count(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', "'{$task->title}' görev durumu güncellendi.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Manager Action: Add New Task
|
||||
*/
|
||||
public function addTask(Request $request, string $slug)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return back()->withErrors(['error' => 'Yetkisiz erişim.']);
|
||||
}
|
||||
|
||||
$project = Project::where('slug', $slug)->firstOrFail();
|
||||
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'nullable|string|max:2000',
|
||||
'status' => 'required|in:todo,in_progress,review,done',
|
||||
'priority' => 'required|in:low,medium,high,urgent',
|
||||
'assigned_person' => 'nullable|string|max:255',
|
||||
'project_module_id' => 'nullable|exists:project_modules,id',
|
||||
]);
|
||||
|
||||
$task = ProjectTask::create([
|
||||
'project_id' => $project->id,
|
||||
'project_module_id' => $request->input('project_module_id'),
|
||||
'title' => $request->input('title'),
|
||||
'description' => $request->input('description'),
|
||||
'status' => $request->input('status'),
|
||||
'priority' => $request->input('priority'),
|
||||
'assigned_person' => $request->input('assigned_person'),
|
||||
]);
|
||||
|
||||
$project->recalculateProgress();
|
||||
|
||||
return back()->with('success', "'{$task->title}' görevi panoya eklendi.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Manager Action: Delete Task
|
||||
*/
|
||||
public function deleteTask(Request $request, string $slug)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json(['success' => false, 'message' => 'Yetkisiz erişim.'], 403);
|
||||
}
|
||||
return back()->withErrors(['error' => 'Yetkisiz erişim.']);
|
||||
}
|
||||
|
||||
$project = Project::where('slug', $slug)->firstOrFail();
|
||||
|
||||
$request->validate([
|
||||
'task_id' => 'required|exists:project_tasks,id',
|
||||
]);
|
||||
|
||||
$task = ProjectTask::where('id', $request->input('task_id'))
|
||||
->where('project_id', $project->id)
|
||||
->firstOrFail();
|
||||
|
||||
$taskName = $task->title;
|
||||
$task->delete();
|
||||
$project->recalculateProgress();
|
||||
|
||||
return back()->with('success', "'{$taskName}' görevi silindi.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Manager Action: Add Live Progress Update / Announcement
|
||||
*/
|
||||
public function addUpdate(Request $request, string $slug)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return back()->withErrors(['error' => 'Yetkisiz erişim.']);
|
||||
}
|
||||
|
||||
$project = Project::where('slug', $slug)->firstOrFail();
|
||||
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'content' => 'required|string|max:5000',
|
||||
]);
|
||||
|
||||
ProjectUpdate::create([
|
||||
'project_id' => $project->id,
|
||||
'user_id' => Auth::id(),
|
||||
'title' => $request->input('title'),
|
||||
'content' => $request->input('content'),
|
||||
'progress_percent_at_update' => $project->progress_percent,
|
||||
'is_public' => true,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Yeni ilerleme duyurusu yayınlandı.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Manager Action: Delete Live Progress Update / Announcement
|
||||
*/
|
||||
public function deleteUpdate(Request $request, string $slug)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json(['success' => false, 'message' => 'Yetkisiz erişim.'], 403);
|
||||
}
|
||||
return back()->withErrors(['error' => 'Yetkisiz erişim.']);
|
||||
}
|
||||
|
||||
$project = Project::where('slug', $slug)->firstOrFail();
|
||||
|
||||
$request->validate([
|
||||
'update_id' => 'required|exists:project_updates,id',
|
||||
]);
|
||||
|
||||
$update = ProjectUpdate::where('id', $request->input('update_id'))
|
||||
->where('project_id', $project->id)
|
||||
->firstOrFail();
|
||||
|
||||
$update->delete();
|
||||
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Duyuru başarıyla silindi.',
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Duyuru başarıyla silindi.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Manager Action: Recalculate Progress %
|
||||
*/
|
||||
public function recalculate(Request $request, string $slug)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return back()->withErrors(['error' => 'Yetkisiz erişim.']);
|
||||
}
|
||||
|
||||
$project = Project::where('slug', $slug)->firstOrFail();
|
||||
$pct = $project->recalculateProgress();
|
||||
|
||||
return back()->with('success', "Proje ilerleme yüzdesi yeniden hesaplandı: %{$pct}");
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Project extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'proposal_id',
|
||||
'title',
|
||||
'slug',
|
||||
'client_name',
|
||||
'client_email',
|
||||
'client_access_code',
|
||||
'status',
|
||||
'progress_percent',
|
||||
'start_date',
|
||||
'target_date',
|
||||
'completed_at',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_date' => 'date',
|
||||
'target_date' => 'date',
|
||||
'completed_at' => 'datetime',
|
||||
'progress_percent' => 'integer',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($project) {
|
||||
if (empty($project->slug)) {
|
||||
$project->slug = Str::slug($project->title) . '-' . Str::random(5);
|
||||
}
|
||||
if (empty($project->client_access_code)) {
|
||||
$project->client_access_code = strtoupper(Str::random(6));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function proposal()
|
||||
{
|
||||
return $this->belongsTo(Proposal::class);
|
||||
}
|
||||
|
||||
public function modules()
|
||||
{
|
||||
return $this->hasMany(ProjectModule::class)->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
public function tasks()
|
||||
{
|
||||
return $this->hasMany(ProjectTask::class)->orderBy('order_index', 'asc');
|
||||
}
|
||||
|
||||
public function updates()
|
||||
{
|
||||
return $this->hasMany(ProjectUpdate::class)->latest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate progress percentage based on completed modules weight
|
||||
*/
|
||||
public function recalculateProgress()
|
||||
{
|
||||
$totalWeight = $this->modules()->sum('weight_percent');
|
||||
if ($totalWeight > 0) {
|
||||
$completedWeight = $this->modules()->where('status', 'completed')->sum('weight_percent');
|
||||
$progress = (int) round(($completedWeight / $totalWeight) * 100);
|
||||
} else {
|
||||
$totalTasks = $this->tasks()->count();
|
||||
if ($totalTasks > 0) {
|
||||
$completedTasks = $this->tasks()->where('status', 'done')->count();
|
||||
$progress = (int) round(($completedTasks / $totalTasks) * 100);
|
||||
} else {
|
||||
$progress = $this->progress_percent;
|
||||
}
|
||||
}
|
||||
|
||||
$this->update(['progress_percent' => min(100, max(0, $progress))]);
|
||||
return $this->progress_percent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProjectModule extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'title',
|
||||
'description',
|
||||
'weight_percent',
|
||||
'status',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'weight_percent' => 'integer',
|
||||
'order' => 'integer',
|
||||
];
|
||||
|
||||
public function project()
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function tasks()
|
||||
{
|
||||
return $this->hasMany(ProjectTask::class, 'project_module_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProjectTask extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'project_module_id',
|
||||
'title',
|
||||
'description',
|
||||
'status',
|
||||
'priority',
|
||||
'due_date',
|
||||
'assigned_person',
|
||||
'order_index',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'due_date' => 'date',
|
||||
'order_index' => 'integer',
|
||||
];
|
||||
|
||||
public function project()
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function module()
|
||||
{
|
||||
return $this->belongsTo(ProjectModule::class, 'project_module_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProjectUpdate extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'user_id',
|
||||
'title',
|
||||
'content',
|
||||
'progress_percent_at_update',
|
||||
'is_public',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'progress_percent_at_update' => 'integer',
|
||||
'is_public' => 'boolean',
|
||||
];
|
||||
|
||||
public function project()
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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('blogs', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
<?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::create('projects', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('proposal_id')->nullable()->constrained('proposals')->nullOnDelete();
|
||||
$table->string('title');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('client_name');
|
||||
$table->string('client_email')->nullable();
|
||||
$table->string('client_access_code')->nullable();
|
||||
$table->enum('status', ['planning', 'in_progress', 'on_hold', 'completed', 'cancelled'])->default('in_progress');
|
||||
$table->unsignedInteger('progress_percent')->default(0);
|
||||
$table->date('start_date')->nullable();
|
||||
$table->date('target_date')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('project_modules', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
$table->unsignedInteger('weight_percent')->default(10);
|
||||
$table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
|
||||
$table->date('start_date')->nullable();
|
||||
$table->date('end_date')->nullable();
|
||||
$table->integer('order')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('project_tasks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
|
||||
$table->foreignId('project_module_id')->nullable()->constrained('project_modules')->nullOnDelete();
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
$table->enum('status', ['todo', 'in_progress', 'review', 'done'])->default('todo');
|
||||
$table->enum('priority', ['low', 'medium', 'high', 'urgent'])->default('medium');
|
||||
$table->date('due_date')->nullable();
|
||||
$table->string('assigned_person')->nullable();
|
||||
$table->integer('order_index')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('project_updates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('title');
|
||||
$table->text('content');
|
||||
$table->unsignedInteger('progress_percent_at_update')->nullable();
|
||||
$table->boolean('is_public')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('project_updates');
|
||||
Schema::dropIfExists('project_tasks');
|
||||
Schema::dropIfExists('project_modules');
|
||||
Schema::dropIfExists('projects');
|
||||
}
|
||||
};
|
||||
@@ -2,5 +2,7 @@ User-agent: *
|
||||
Allow: /
|
||||
Disallow: /admin/
|
||||
Disallow: /stajyer/admin/
|
||||
Disallow: /teklif/
|
||||
Disallow: /proje-takip/
|
||||
|
||||
Sitemap: https://truncgil.com/sitemap.xml
|
||||
|
||||
@@ -23,6 +23,14 @@
|
||||
<i class="uil uil-list-ul text-lg"></i>
|
||||
<span>Stajyer Listesi ve Detaylı Takip</span>
|
||||
</button>
|
||||
|
||||
<button type="button" role="tab" aria-selected="false" data-tab-target="quick-approval" 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 relative">
|
||||
<i class="uil uil-check-square text-lg"></i>
|
||||
<span>Hızlı Defter Onaylama</span>
|
||||
<span id="quick-approval-badge" class="{{ $unapprovedCount > 0 ? '' : 'hidden' }} absolute -top-1 -right-1 flex h-5 min-w-[20px] items-center justify-center rounded-full bg-rose-500 px-1.5 text-[10px] font-black text-white ring-2 ring-white shadow-md">
|
||||
{{ $unapprovedCount }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +43,10 @@
|
||||
@include('front.career.partials.list')
|
||||
</div>
|
||||
|
||||
<div id="quick-approval-panel" class="admin-tab-panel hidden space-y-6">
|
||||
@include('front.career.partials.quick_approval')
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -632,6 +644,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetId === 'quick-approval') {
|
||||
if (typeof loadQuickApprovalEntries === 'function') {
|
||||
loadQuickApprovalEntries();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -377,6 +377,23 @@
|
||||
<span class="block text-xs text-slate-400 font-semibold mt-0.5">Günlük çalışma defteri</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Menu Item 4: Blog Yazıları -->
|
||||
@php
|
||||
$approvedBlogsCount = isset($blogs) ? $blogs->where('status', 'published')->count() : 0;
|
||||
@endphp
|
||||
<button type="button" role="tab" aria-selected="false" data-tab-target="blogs" class="tab-btn w-full flex items-center gap-4 p-3 rounded-2xl text-left transition-all border border-transparent">
|
||||
<div class="w-10 h-10 rounded-xl bg-slate-50 text-slate-500 flex items-center justify-center text-xl flex-shrink-0 tab-icon">
|
||||
<i class="uil uil-newspaper"></i>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="block text-sm font-extrabold text-slate-800">Blog Yazıları</span>
|
||||
<span class="text-[10px] font-extrabold px-2 py-0.5 rounded-full @if($approvedBlogsCount >= 3) bg-green-100 text-green-700 @else bg-blue-100 text-blue-700 @endif">{{ $approvedBlogsCount }}/3</span>
|
||||
</div>
|
||||
<span class="block text-xs text-slate-400 font-semibold mt-0.5">Sitede yayınlanacak bloglar</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -785,6 +802,222 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panel 4: Blog Yazıları -->
|
||||
<div id="blogs-panel" class="tab-panel hidden space-y-6">
|
||||
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50">
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 pb-6 border-b border-slate-100">
|
||||
<div>
|
||||
<h3 class="font-bold text-slate-800 text-lg flex items-center gap-2">
|
||||
<i class="uil uil-newspaper text-blue-600"></i>
|
||||
<span>Staj Blog Yazıları (Min. 3 Adet)</span>
|
||||
</h3>
|
||||
<p class="text-xs text-slate-400 mt-1">
|
||||
Staj süresince en az 3 blog yazısı kaleme almalısınız. Onaylanan yazılar web sitemizin blog bölümünde yayınlanacaktır.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" onclick="openBlogModal()" class="px-4 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 flex items-center gap-2">
|
||||
<i class="uil uil-plus text-base"></i>
|
||||
<span>Yeni Blog Yazısı Ekle</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
@php
|
||||
$userBlogs = $blogs ?? collect();
|
||||
$publishedBlogs = $userBlogs->where('status', 'published');
|
||||
$publishedCount = $publishedBlogs->count();
|
||||
$progressPercent = min(100, round(($publishedCount / 3) * 100));
|
||||
@endphp
|
||||
<div class="mt-6 p-5 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl border border-blue-100">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-xs font-extrabold text-blue-900">Blog Görev Tamamlama Oranı</span>
|
||||
<span class="text-xs font-black text-blue-700">{{ $publishedCount }} / 3 Yayınlandı (%{{ $progressPercent }})</span>
|
||||
</div>
|
||||
<div class="w-full bg-blue-200/60 rounded-full h-2.5 overflow-hidden">
|
||||
<div class="bg-blue-600 h-2.5 rounded-full transition-all duration-500" style="width: {{ $progressPercent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3 Mandatory Category Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||
@php
|
||||
$expBlog = $userBlogs->where('intern_category', 'experience')->first();
|
||||
$techBlog = $userBlogs->where('intern_category', 'technical_challenge')->first();
|
||||
$prodBlog = $userBlogs->where('intern_category', 'product_showcase')->first();
|
||||
@endphp
|
||||
|
||||
<!-- Category 1 -->
|
||||
<div class="p-4 rounded-2xl border border-slate-100 bg-slate-50/50 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-[10px] uppercase font-extrabold tracking-wider text-slate-400">1. Konu</span>
|
||||
@if($expBlog && $expBlog->status === 'published')
|
||||
<span class="px-2 py-0.5 bg-green-50 text-green-700 border border-green-200 text-[10px] font-bold rounded-lg">Yayınlandı ✓</span>
|
||||
@elseif($expBlog && $expBlog->status === 'pending')
|
||||
<span class="px-2 py-0.5 bg-amber-50 text-amber-700 border border-amber-200 text-[10px] font-bold rounded-lg">Onay Bekliyor</span>
|
||||
@elseif($expBlog && $expBlog->status === 'rejected')
|
||||
<span class="px-2 py-0.5 bg-red-50 text-red-700 border border-red-200 text-[10px] font-bold rounded-lg">Revize İstendi</span>
|
||||
@elseif($expBlog && $expBlog->status === 'draft')
|
||||
<span class="px-2 py-0.5 bg-slate-100 text-slate-600 border border-slate-200 text-[10px] font-bold rounded-lg">Taslak</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 bg-slate-100 text-slate-400 border border-slate-200 text-[10px] font-bold rounded-lg">Bekliyor</span>
|
||||
@endif
|
||||
</div>
|
||||
<h4 class="text-xs font-extrabold text-slate-800">Staj Tecrübesi & Adaptasyon</h4>
|
||||
<p class="text-[11px] text-slate-400 mt-1">Staj süreci, şirket kültürü ve ilk izlenimlerinizi anlatan yazı.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category 2 -->
|
||||
<div class="p-4 rounded-2xl border border-slate-100 bg-slate-50/50 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-[10px] uppercase font-extrabold tracking-wider text-slate-400">2. Konu</span>
|
||||
@if($techBlog && $techBlog->status === 'published')
|
||||
<span class="px-2 py-0.5 bg-green-50 text-green-700 border border-green-200 text-[10px] font-bold rounded-lg">Yayınlandı ✓</span>
|
||||
@elseif($techBlog && $techBlog->status === 'pending')
|
||||
<span class="px-2 py-0.5 bg-amber-50 text-amber-700 border border-amber-200 text-[10px] font-bold rounded-lg">Onay Bekliyor</span>
|
||||
@elseif($techBlog && $techBlog->status === 'rejected')
|
||||
<span class="px-2 py-0.5 bg-red-50 text-red-700 border border-red-200 text-[10px] font-bold rounded-lg">Revize İstendi</span>
|
||||
@elseif($techBlog && $techBlog->status === 'draft')
|
||||
<span class="px-2 py-0.5 bg-slate-100 text-slate-600 border border-slate-200 text-[10px] font-bold rounded-lg">Taslak</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 bg-slate-100 text-slate-400 border border-slate-200 text-[10px] font-bold rounded-lg">Bekliyor</span>
|
||||
@endif
|
||||
</div>
|
||||
<h4 class="text-xs font-extrabold text-slate-800">Teknik Zorluklar & Çözümler</h4>
|
||||
<p class="text-[11px] text-slate-400 mt-1">Gelişim sürecinde karşılaştığınız teknik engeller ve çözüm yöntemleri.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category 3 -->
|
||||
<div class="p-4 rounded-2xl border border-slate-100 bg-slate-50/50 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-[10px] uppercase font-extrabold tracking-wider text-slate-400">3. Konu</span>
|
||||
@if($prodBlog && $prodBlog->status === 'published')
|
||||
<span class="px-2 py-0.5 bg-green-50 text-green-700 border border-green-200 text-[10px] font-bold rounded-lg">Yayınlandı ✓</span>
|
||||
@elseif($prodBlog && $prodBlog->status === 'pending')
|
||||
<span class="px-2 py-0.5 bg-amber-50 text-amber-700 border border-amber-200 text-[10px] font-bold rounded-lg">Onay Bekliyor</span>
|
||||
@elseif($prodBlog && $prodBlog->status === 'rejected')
|
||||
<span class="px-2 py-0.5 bg-red-50 text-red-700 border border-red-200 text-[10px] font-bold rounded-lg">Revize İstendi</span>
|
||||
@elseif($prodBlog && $prodBlog->status === 'draft')
|
||||
<span class="px-2 py-0.5 bg-slate-100 text-slate-600 border border-slate-200 text-[10px] font-bold rounded-lg">Taslak</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 bg-slate-100 text-slate-400 border border-slate-200 text-[10px] font-bold rounded-lg">Bekliyor</span>
|
||||
@endif
|
||||
</div>
|
||||
<h4 class="text-xs font-extrabold text-slate-800">Ürün / Proje Tanıtımı (Showcase)</h4>
|
||||
<p class="text-[11px] text-slate-400 mt-1">Geliştirdiğiniz nihai ürünün amacı, mimarisi ve canlı demo anlatımı.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog List Table -->
|
||||
<div class="mt-8">
|
||||
<h4 class="text-sm font-extrabold text-slate-800 mb-4">Gönderilen Blog Yazılarınız</h4>
|
||||
|
||||
@if($userBlogs->isEmpty())
|
||||
<div class="text-center py-10 border-2 border-dashed border-slate-100 rounded-2xl">
|
||||
<i class="uil uil-file-edit-alt text-4xl text-slate-300"></i>
|
||||
<p class="text-xs text-slate-500 font-bold mt-2">Henüz bir blog yazısı eklemediniz.</p>
|
||||
<button type="button" onclick="openBlogModal()" class="mt-3 text-xs font-extrabold text-blue-600 hover:text-blue-800">
|
||||
+ İlk Blog Yazısını Ekle
|
||||
</button>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-100 text-[11px] font-extrabold text-slate-400 uppercase tracking-wider">
|
||||
<th class="py-3 px-3">Görsel</th>
|
||||
<th class="py-3 px-3">Başlık & Konu</th>
|
||||
<th class="py-3 px-3">Durum</th>
|
||||
<th class="py-3 px-3">Yönetici Notu</th>
|
||||
<th class="py-3 px-3 text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 text-xs">
|
||||
@foreach($userBlogs as $blogItem)
|
||||
<tr>
|
||||
<td class="py-3 px-3">
|
||||
@if($blogItem->featured_image)
|
||||
<img src="{{ asset('storage/' . $blogItem->featured_image) }}" class="w-12 h-12 object-cover rounded-xl border border-slate-100" />
|
||||
@else
|
||||
<div class="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center text-slate-400">
|
||||
<i class="uil uil-image text-xl"></i>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 px-3">
|
||||
<span class="font-bold text-slate-800 block text-sm">{{ $blogItem->title }}</span>
|
||||
<span class="text-[10px] font-bold text-slate-400 block mt-0.5">
|
||||
@if($blogItem->intern_category === 'experience') 1. Staj Tecrübesi
|
||||
@elseif($blogItem->intern_category === 'technical_challenge') 2. Teknik Zorluklar
|
||||
@elseif($blogItem->intern_category === 'product_showcase') 3. Ürün Tanıtımı
|
||||
@else {{ $blogItem->intern_category }} @endif
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 px-3">
|
||||
@if($blogItem->status === 'published')
|
||||
<span class="px-2.5 py-1 bg-green-50 text-green-700 border border-green-200 font-extrabold rounded-full text-[11px] inline-flex items-center gap-1">
|
||||
<i class="uil uil-check-circle text-sm"></i> Sitede Yayınlandı
|
||||
</span>
|
||||
@elseif($blogItem->status === 'pending')
|
||||
<span class="px-2.5 py-1 bg-amber-50 text-amber-700 border border-amber-200 font-extrabold rounded-full text-[11px] inline-flex items-center gap-1">
|
||||
<i class="uil uil-clock text-sm"></i> Onay Bekliyor
|
||||
</span>
|
||||
@elseif($blogItem->status === 'rejected')
|
||||
<span class="px-2.5 py-1 bg-red-50 text-red-700 border border-red-200 font-extrabold rounded-full text-[11px] inline-flex items-center gap-1">
|
||||
<i class="uil uil-exclamation-triangle text-sm"></i> Revize İstendi
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2.5 py-1 bg-slate-100 text-slate-600 border border-slate-200 font-extrabold rounded-full text-[11px] inline-flex items-center gap-1">
|
||||
<i class="uil uil-edit text-sm"></i> Taslak
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 px-3">
|
||||
@if($blogItem->admin_feedback)
|
||||
<div class="p-2 bg-red-50 border border-red-100 rounded-xl text-red-700 text-[11px] font-semibold max-w-xs">
|
||||
<strong>Geribildirim:</strong> {{ $blogItem->admin_feedback }}
|
||||
</div>
|
||||
@else
|
||||
<span class="text-slate-300">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 px-3 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if($blogItem->status === 'published')
|
||||
<a href="{{ route('blog.show', $blogItem->slug) }}" target="_blank" class="px-3 py-1.5 bg-green-50 text-green-700 hover:bg-green-100 font-bold rounded-lg text-xs transition-all flex items-center gap-1">
|
||||
<i class="uil uil-external-link-alt"></i> Sitede Gör
|
||||
</a>
|
||||
@else
|
||||
<button type="button" onclick='editBlogItem({{ json_encode($blogItem) }})' class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 font-bold rounded-lg text-xs transition-all flex items-center gap-1">
|
||||
<i class="uil uil-pen"></i> Düzenle
|
||||
</button>
|
||||
<form action="{{ route('intern.blog.delete', $blogItem->id) }}" method="POST" onsubmit="return confirm('Bu blog yazısını silmek istediğinize emin misiniz?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="px-2.5 py-1.5 bg-slate-100 text-red-600 hover:bg-red-50 font-bold rounded-lg text-xs transition-all">
|
||||
<i class="uil uil-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1485,6 +1718,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isTurkeyHoliday(dateObj) {
|
||||
const yyyy = dateObj.getFullYear();
|
||||
const mm = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(dateObj.getDate()).padStart(2, '0');
|
||||
const ymd = `${yyyy}-${mm}-${dd}`;
|
||||
const md = `${mm}-${dd}`;
|
||||
|
||||
const fixedHolidays = [
|
||||
'01-01', '04-23', '05-01', '05-19', '07-15', '08-30', '10-29'
|
||||
];
|
||||
|
||||
if (fixedHolidays.includes(md)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const variableHolidays = [
|
||||
'2024-04-09', '2024-04-10', '2024-04-11', '2024-04-12',
|
||||
'2024-06-15', '2024-06-16', '2024-06-17', '2024-06-18', '2024-06-19',
|
||||
'2025-03-29', '2025-03-30', '2025-03-31', '2025-04-01',
|
||||
'2025-06-05', '2025-06-06', '2025-06-07', '2025-06-08', '2025-06-09',
|
||||
'2026-03-19', '2026-03-20', '2026-03-21', '2026-03-22',
|
||||
'2026-05-26', '2026-05-27', '2026-05-28', '2026-05-29', '2026-05-30',
|
||||
'2027-03-08', '2027-03-09', '2027-03-10', '2027-03-11',
|
||||
'2027-05-15', '2027-05-16', '2027-05-17', '2027-05-18', '2027-05-19',
|
||||
'2028-02-26', '2028-02-27', '2028-02-28', '2028-02-29',
|
||||
'2028-05-04', '2028-05-05', '2028-05-06', '2028-05-07', '2028-05-08',
|
||||
'2029-02-14', '2029-02-15', '2029-02-16', '2029-02-17',
|
||||
'2029-04-23', '2029-04-24', '2029-04-25', '2029-04-26', '2029-04-27',
|
||||
'2030-02-03', '2030-02-04', '2030-02-05', '2030-02-06',
|
||||
'2030-04-12', '2030-04-13', '2030-04-14', '2030-04-15', '2030-04-16'
|
||||
];
|
||||
|
||||
return variableHolidays.includes(ymd);
|
||||
}
|
||||
|
||||
function calculateEndDateFrontend() {
|
||||
const startDateVal = document.getElementById('internship_start_date').value;
|
||||
const totalDaysVal = document.getElementById('internship_total_days').value;
|
||||
@@ -1501,7 +1769,7 @@
|
||||
|
||||
while (count < daysToAdd) {
|
||||
const dayOfWeek = date.getDay();
|
||||
if (dayOfWeek === 6 || dayOfWeek === 0) { // 6 = Saturday, 0 = Sunday
|
||||
if (dayOfWeek === 6 || dayOfWeek === 0 || isTurkeyHoliday(date)) { // 6 = Saturday, 0 = Sunday
|
||||
date.setDate(date.getDate() + 1);
|
||||
continue;
|
||||
}
|
||||
@@ -1528,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 === '<p><br></p>') {
|
||||
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();
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.shadow-xl {
|
||||
@@ -1557,5 +1908,75 @@
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
<!-- Blog Creation/Edit Modal -->
|
||||
<div id="blog-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 md:p-8 max-w-3xl w-full shadow-2xl border border-slate-100/50 transform scale-95 opacity-0 transition-all duration-300 max-h-[90vh] overflow-y-auto" id="blog-modal-card">
|
||||
<div class="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-extrabold text-slate-800 flex items-center gap-2" id="blog-modal-title">
|
||||
<i class="uil uil-pen text-blue-600"></i>
|
||||
<span>Yeni Blog Yazısı Ekle</span>
|
||||
</h3>
|
||||
<p class="text-xs text-slate-400 mt-1">Yazınız yöneticiniz tarafından onaylandıktan sonra web sitesinde yayınlanacaktır.</p>
|
||||
</div>
|
||||
<button type="button" onclick="closeBlogModal()" class="text-slate-400 hover:text-slate-600">
|
||||
<i class="uil uil-multiply text-2xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="blog-form" action="{{ route('intern.blog.save') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<input type="hidden" name="blog_id" id="modal_blog_id" value="" />
|
||||
<input type="hidden" name="action_type" id="modal_action_type" value="submit" />
|
||||
<input type="hidden" name="content" id="modal_blog_content" value="" />
|
||||
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-xs font-extrabold text-slate-600 uppercase tracking-wider mb-2">Blog Başlığı *</label>
|
||||
<input type="text" name="title" id="modal_blog_title" required class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 text-sm font-bold text-slate-800" placeholder="Örn: Uzaktan Staj Deneyimim ve Karşılaştığım Zorluklar" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-extrabold text-slate-600 uppercase tracking-wider mb-2">Staj Konu Kategorisi *</label>
|
||||
<select name="intern_category" id="modal_intern_category" required class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 text-sm font-bold text-slate-800">
|
||||
<option value="experience">1. Staj Tecrübesi & Adaptasyon</option>
|
||||
<option value="technical_challenge">2. Teknik Zorluklar & Çözümler</option>
|
||||
<option value="product_showcase">3. Ürün / Proje Tanıtımı (Showcase)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-extrabold text-slate-600 uppercase tracking-wider mb-2">Kapak Görseli</label>
|
||||
<input type="file" name="featured_image" id="modal_featured_image" accept="image/*" class="w-full text-xs text-slate-500 file:mr-4 file:py-2.5 file:px-4 file:rounded-xl file:border-0 file:text-xs file:font-bold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-extrabold text-slate-600 uppercase tracking-wider mb-2">Kısa Özet (Excerpt)</label>
|
||||
<textarea name="excerpt" id="modal_blog_excerpt" rows="2" class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 text-xs font-medium text-slate-700" placeholder="Yazınızın liste sayfalarında görünecek kısa özeti..."></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-extrabold text-slate-600 uppercase tracking-wider mb-2">Blog İçeriği *</label>
|
||||
<div id="blog-quill-editor-container" class="rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div id="blog-quill-editor" style="height: 250px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-end gap-3 mt-8 pt-4 border-t border-slate-100">
|
||||
<button type="button" onclick="submitBlogForm('draft')" class="px-5 py-3 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 font-extrabold text-xs transition-all">
|
||||
Taslak Olarak Kaydet
|
||||
</button>
|
||||
<button type="button" onclick="submitBlogForm('submit')" class="px-6 py-3 rounded-xl bg-blue-600 hover:bg-blue-700 text-white font-extrabold text-xs transition-all shadow-md shadow-blue-500/10 flex items-center justify-center gap-2">
|
||||
<i class="uil uil-arrow-right text-base"></i>
|
||||
<span>Onaya Gönder</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('front.career.partials.guide_modal')
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
<!-- Quick Approval Management View -->
|
||||
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50">
|
||||
|
||||
<!-- Header & Description -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-8 pb-6 border-b border-slate-100">
|
||||
<div>
|
||||
<h3 class="font-bold text-slate-800 text-lg flex items-center gap-2">
|
||||
<i class="uil uil-check-square text-[#e31e24] text-xl"></i>
|
||||
<span>Hızlı Defter Onaylama</span>
|
||||
</h3>
|
||||
<p class="text-xs text-slate-400 font-semibold mt-1">Stajyerlerin günlük yazdığı raporları tek bir ekrandan inceleyin ve onaylayın.</p>
|
||||
</div>
|
||||
|
||||
<!-- Filter Options -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="flex bg-slate-50 p-1 rounded-xl border border-slate-200/50">
|
||||
<button type="button" onclick="setQuickFilter('unapproved')" id="qf-btn-unapproved" class="px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer bg-white text-blue-600 shadow-sm border border-slate-200/20">
|
||||
Onay Bekleyenler
|
||||
</button>
|
||||
<button type="button" onclick="setQuickFilter('today')" id="qf-btn-today" class="px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer text-slate-600 hover:text-slate-900">
|
||||
Bugün Yazılanlar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Date Picker for custom date -->
|
||||
<div id="qf-date-wrapper" class="hidden">
|
||||
<input type="date" id="qf-date-input" onchange="loadQuickApprovalEntries()" value="{{ date('Y-m-d') }}" class="text-xs font-semibold text-slate-700 bg-white border border-slate-200 rounded-xl px-3 py-2 focus:outline-none focus:border-blue-400">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="qa-loading" class="flex flex-col items-center justify-center py-16">
|
||||
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 mb-3"></div>
|
||||
<p class="text-xs font-semibold text-slate-400">Defter kayıtları yükleniyor...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="qa-empty" class="hidden flex flex-col items-center justify-center py-16 text-center">
|
||||
<div class="w-16 h-16 bg-emerald-50 rounded-2xl text-emerald-500 flex items-center justify-center text-3xl mb-4 border border-emerald-100">
|
||||
<i class="uil uil-smile-beam"></i>
|
||||
</div>
|
||||
<h4 id="qa-empty-title" class="font-extrabold text-slate-800 text-sm">Harika! Onay Bekleyen Defter Yok</h4>
|
||||
<p id="qa-empty-desc" class="text-xs text-slate-400 font-semibold mt-1">Şu anda onayınızı bekleyen hiçbir stajyer raporu bulunmuyor.</p>
|
||||
</div>
|
||||
|
||||
<!-- Content List Grid -->
|
||||
<div id="qa-list-grid" class="hidden grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Loaded dynamically -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Dedicated Quick Approval Entry Reading Modal -->
|
||||
<div id="quick-entry-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-[85vh]" id="quick-entry-modal-card">
|
||||
|
||||
<!-- 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="qem-intern-name" class="font-bold text-slate-800 text-sm">Stajyer İsmi</h4>
|
||||
<p id="qem-meta" class="text-xs text-slate-400 font-semibold mt-0.5"></p>
|
||||
</div>
|
||||
<button type="button" onclick="closeQuickEntryModal()" 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 flex flex-col min-h-[250px]">
|
||||
<div id="qem-retroactive-badge" class="mb-4">
|
||||
<!-- Retroactive indicator -->
|
||||
</div>
|
||||
<div id="qem-content" class="prose max-w-none text-slate-700 leading-relaxed text-sm select-text flex-grow">
|
||||
<!-- Full text rendered safely -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Controls -->
|
||||
<div class="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-4 flex-shrink-0">
|
||||
<!-- Carousel navigation -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" id="qem-btn-prev" onclick="prevQuickEntry()" class="px-3 py-1.5 bg-white hover:bg-slate-100 border border-slate-200 text-slate-600 rounded-xl text-xs font-bold transition-all flex items-center gap-1 cursor-pointer">
|
||||
<i class="uil uil-angle-left-b text-sm"></i> Önceki
|
||||
</button>
|
||||
<span id="qem-indicator" class="text-xs font-extrabold text-slate-500 bg-slate-200/50 px-2.5 py-1.5 rounded-lg">0 / 0</span>
|
||||
<button type="button" id="qem-btn-next" onclick="nextQuickEntry()" class="px-3 py-1.5 bg-white hover:bg-slate-100 border border-slate-200 text-slate-600 rounded-xl text-xs font-bold transition-all flex items-center gap-1 cursor-pointer">
|
||||
Sonraki <i class="uil uil-angle-right-b text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" id="qem-action-btn" onclick="toggleQuickEntryApproval()" class="px-5 py-2.5 text-white rounded-xl font-bold text-xs shadow-lg transition-all cursor-pointer">
|
||||
Onayla
|
||||
</button>
|
||||
<button type="button" onclick="closeQuickEntryModal()" class="px-4 py-2.5 bg-slate-800 hover:bg-slate-900 text-white font-bold rounded-xl text-xs transition-colors cursor-pointer">Kapat</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Safe DOM HTML Sanitizer script -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.6/purify.min.js" integrity="sha384-+VfUPEb0PdtChMwmBcBmykRMDd+v6D/oFmB3rZM/puCMDYcIvF968OimRh4KQY9a" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script>
|
||||
let quickApprovalEntries = [];
|
||||
let currentQuickFilter = 'unapproved';
|
||||
let activeQuickEntryIndex = -1;
|
||||
let isUpdatingQuickApproval = false;
|
||||
|
||||
function setQuickFilter(filter) {
|
||||
currentQuickFilter = filter;
|
||||
|
||||
// Toggle active state classes on filter buttons
|
||||
const btnUnapproved = document.getElementById('qf-btn-unapproved');
|
||||
const btnToday = document.getElementById('qf-btn-today');
|
||||
const dateWrapper = document.getElementById('qf-date-wrapper');
|
||||
|
||||
if (filter === 'unapproved') {
|
||||
btnUnapproved.className = "px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer bg-white text-blue-600 shadow-sm border border-slate-200/20";
|
||||
btnToday.className = "px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer text-slate-600 hover:text-slate-900";
|
||||
dateWrapper.classList.add('hidden');
|
||||
} else {
|
||||
btnToday.className = "px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer bg-white text-blue-600 shadow-sm border border-slate-200/20";
|
||||
btnUnapproved.className = "px-4 py-2 text-xs font-bold rounded-lg transition-all cursor-pointer text-slate-600 hover:text-slate-900";
|
||||
dateWrapper.classList.remove('hidden');
|
||||
}
|
||||
|
||||
loadQuickApprovalEntries();
|
||||
}
|
||||
|
||||
function loadQuickApprovalEntries() {
|
||||
const listGrid = document.getElementById('qa-list-grid');
|
||||
const loadingEl = document.getElementById('qa-loading');
|
||||
const emptyEl = document.getElementById('qa-empty');
|
||||
|
||||
listGrid.classList.add('hidden');
|
||||
emptyEl.classList.add('hidden');
|
||||
loadingEl.classList.remove('hidden');
|
||||
|
||||
let url = `/stajyer/admin/quick-approval-entries?type=${currentQuickFilter}`;
|
||||
if (currentQuickFilter === 'today') {
|
||||
const selectedDate = document.getElementById('qf-date-input').value;
|
||||
if (selectedDate) {
|
||||
url += `&date=${selectedDate}`;
|
||||
}
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
loadingEl.classList.add('hidden');
|
||||
if (!data.success) {
|
||||
alert('Kayıtlar yüklenemedi.');
|
||||
return;
|
||||
}
|
||||
|
||||
quickApprovalEntries = data.entries;
|
||||
|
||||
// Update unapproved badge dynamic count if filter is unapproved
|
||||
if (currentQuickFilter === 'unapproved') {
|
||||
updateBadgeCount(quickApprovalEntries.length);
|
||||
}
|
||||
|
||||
if (quickApprovalEntries.length === 0) {
|
||||
const emptyTitle = document.getElementById('qa-empty-title');
|
||||
const emptyDesc = document.getElementById('qa-empty-desc');
|
||||
|
||||
if (currentQuickFilter === 'unapproved') {
|
||||
emptyTitle.textContent = "Onay Bekleyen Defter Yok";
|
||||
emptyDesc.textContent = "Harika! Şu anda onayınızı bekleyen hiçbir stajyer raporu bulunmuyor.";
|
||||
} else {
|
||||
emptyTitle.textContent = "Kayıt Bulunamadı";
|
||||
emptyDesc.textContent = "Seçilen tarihte yazılmış herhangi bir stajyer defteri kaydı bulunmuyor.";
|
||||
}
|
||||
|
||||
emptyEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
renderQuickListGrid();
|
||||
listGrid.classList.remove('hidden');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
loadingEl.classList.add('hidden');
|
||||
alert('Sunucuyla bağlantı kurulurken bir hata oluştu.');
|
||||
});
|
||||
}
|
||||
|
||||
function renderQuickListGrid() {
|
||||
const grid = document.getElementById('qa-list-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
quickApprovalEntries.forEach((entry, index) => {
|
||||
// Helper to strip HTML tags for a clean content preview snippet
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = entry.content;
|
||||
const cleanText = tempDiv.textContent || tempDiv.innerText || "";
|
||||
const snippet = cleanText.length > 120 ? cleanText.substring(0, 120) + '...' : cleanText;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.id = `qa-card-${entry.id}`;
|
||||
card.className = "bg-slate-50/50 rounded-2xl p-5 border border-slate-200/40 hover:border-blue-200 hover:bg-blue-50/5 transition-all flex flex-col justify-between shadow-sm relative";
|
||||
|
||||
let badgeHtml = '';
|
||||
if (entry.supervisor_approved) {
|
||||
badgeHtml = `<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase bg-emerald-50 text-emerald-700 border border-emerald-100">ONAYLI</span>`;
|
||||
} else {
|
||||
badgeHtml = `<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase bg-amber-50 text-amber-700 border border-amber-200">ONAY BEKLİYOR</span>`;
|
||||
}
|
||||
|
||||
let retroHtml = '';
|
||||
if (entry.is_retroactive) {
|
||||
retroHtml = `<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase bg-rose-50 text-rose-600 border border-rose-100">Geriye Dönük</span>`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="mb-4">
|
||||
<!-- Top Row -->
|
||||
<div class="flex items-start justify-between gap-2 mb-3">
|
||||
<div>
|
||||
<h4 class="font-extrabold text-slate-800 text-sm hover:underline hover:text-blue-600 cursor-pointer" onclick="openInternJournalModal(${entry.intern.id})">
|
||||
${escapeHTML(entry.intern.name)}
|
||||
</h4>
|
||||
<span class="text-[10px] text-slate-400 font-semibold block mt-0.5">${escapeHTML(entry.intern.email)}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-1.5">
|
||||
${badgeHtml}
|
||||
${retroHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Day & Date Info -->
|
||||
<div class="mb-3 text-[11px] font-extrabold text-blue-600 bg-blue-50/50 px-2.5 py-1.5 rounded-xl w-max">
|
||||
${entry.day_number}. Gün (${entry.formatted_date})
|
||||
</div>
|
||||
|
||||
<!-- Snippet Content Preview -->
|
||||
<p class="text-xs text-slate-600 leading-relaxed font-medium bg-white p-3.5 rounded-xl border border-slate-100/80 min-h-[60px] italic">
|
||||
"${escapeHTML(snippet)}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card Actions -->
|
||||
<div class="pt-3 border-t border-slate-200/50 flex items-center justify-between gap-3">
|
||||
<button type="button" onclick="openQuickEntryModal(${index})" class="px-3.5 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold rounded-xl text-[11px] transition-all cursor-pointer flex items-center gap-1.5">
|
||||
<i class="uil uil-book-open"></i> Oku & Onayla
|
||||
</button>
|
||||
|
||||
${!entry.supervisor_approved ? `
|
||||
<button type="button" id="quick-approve-btn-${entry.id}" onclick="quickApproveEntry(${entry.id}, ${index})" class="px-3.5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white font-bold rounded-xl text-[11px] transition-all shadow-md shadow-emerald-500/10 cursor-pointer flex items-center gap-1">
|
||||
<i class="uil uil-check"></i> Hızlı Onayla
|
||||
</button>
|
||||
` : `
|
||||
<button type="button" id="quick-approve-btn-${entry.id}" onclick="quickApproveEntry(${entry.id}, ${index})" class="px-3.5 py-2 bg-rose-50 hover:bg-rose-100 text-rose-600 border border-rose-200 font-bold rounded-xl text-[11px] transition-all cursor-pointer flex items-center gap-1">
|
||||
<i class="uil uil-times"></i> Onayı Kaldır
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function updateBadgeCount(count) {
|
||||
const badge = document.getElementById('quick-approval-badge');
|
||||
if (badge) {
|
||||
badge.textContent = count;
|
||||
if (count > 0) {
|
||||
badge.classList.remove('hidden');
|
||||
} else {
|
||||
badge.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function quickApproveEntry(entryId, index) {
|
||||
const btn = document.getElementById(`quick-approve-btn-${entryId}`);
|
||||
if (!btn || isUpdatingQuickApproval) return;
|
||||
|
||||
isUpdatingQuickApproval = true;
|
||||
btn.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: entryId
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Update entry status in local array
|
||||
quickApprovalEntries[index].supervisor_approved = data.status;
|
||||
quickApprovalEntries[index].supervisor_name = data.supervisor_name;
|
||||
|
||||
// If in "unapproved" view, and we approved, we can slide out the card nicely
|
||||
if (currentQuickFilter === 'unapproved' && data.status) {
|
||||
const card = document.getElementById(`qa-card-${entryId}`);
|
||||
if (card) {
|
||||
card.style.transition = 'all 0.3s ease-out';
|
||||
card.style.transform = 'scale(0.95)';
|
||||
card.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
// Remove item from array and re-render or just remove the element
|
||||
quickApprovalEntries = quickApprovalEntries.filter(e => e.id !== entryId);
|
||||
updateBadgeCount(quickApprovalEntries.length);
|
||||
|
||||
if (quickApprovalEntries.length === 0) {
|
||||
document.getElementById('qa-list-grid').classList.add('hidden');
|
||||
document.getElementById('qa-empty').classList.remove('hidden');
|
||||
} else {
|
||||
renderQuickListGrid();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
} else {
|
||||
// Just re-render list
|
||||
renderQuickListGrid();
|
||||
}
|
||||
} else {
|
||||
alert(data.message || 'Onay güncellenemedi.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert('İşlem gerçekleştirilemedi.');
|
||||
})
|
||||
.finally(() => {
|
||||
isUpdatingQuickApproval = false;
|
||||
if (btn) btn.style.opacity = '1';
|
||||
});
|
||||
}
|
||||
|
||||
// Quick entry modal logic
|
||||
function openQuickEntryModal(index) {
|
||||
activeQuickEntryIndex = index;
|
||||
|
||||
const modal = document.getElementById('quick-entry-modal');
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
|
||||
const card = document.getElementById('quick-entry-modal-card');
|
||||
if (card) {
|
||||
setTimeout(() => {
|
||||
card.classList.remove('scale-95', 'opacity-0');
|
||||
card.classList.add('scale-100', 'opacity-100');
|
||||
}, 50);
|
||||
}
|
||||
|
||||
renderQuickEntryDetails();
|
||||
}
|
||||
|
||||
function closeQuickEntryModal() {
|
||||
const modal = document.getElementById('quick-entry-modal');
|
||||
if (!modal) return;
|
||||
|
||||
const card = document.getElementById('quick-entry-modal-card');
|
||||
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 renderQuickEntryDetails() {
|
||||
if (activeQuickEntryIndex < 0 || activeQuickEntryIndex >= quickApprovalEntries.length) return;
|
||||
|
||||
const entry = quickApprovalEntries[activeQuickEntryIndex];
|
||||
|
||||
// Header details
|
||||
document.getElementById('qem-intern-name').textContent = entry.intern.name;
|
||||
document.getElementById('qem-meta').textContent = `${entry.day_number}. Gün - ${entry.formatted_date}`;
|
||||
|
||||
// Retroactive badge
|
||||
const retroBadge = document.getElementById('qem-retroactive-badge');
|
||||
retroBadge.innerHTML = '';
|
||||
if (entry.is_retroactive) {
|
||||
retroBadge.innerHTML = `
|
||||
<span class="inline-flex items-center gap-1 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
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Body content (Sanitized using DOMPurify!)
|
||||
const cleanHTML = DOMPurify.sanitize(entry.content);
|
||||
document.getElementById('qem-content').innerHTML = cleanHTML || '<p class="text-slate-400 italic">Boş içerik.</p>';
|
||||
|
||||
// Carousel buttons indicators
|
||||
document.getElementById('qem-indicator').textContent = `${activeQuickEntryIndex + 1} / ${quickApprovalEntries.length}`;
|
||||
|
||||
const btnPrev = document.getElementById('qem-btn-prev');
|
||||
const btnNext = document.getElementById('qem-btn-next');
|
||||
|
||||
btnPrev.disabled = activeQuickEntryIndex === 0;
|
||||
btnPrev.style.opacity = activeQuickEntryIndex === 0 ? '0.4' : '1';
|
||||
btnPrev.style.cursor = activeQuickEntryIndex === 0 ? 'not-allowed' : 'pointer';
|
||||
|
||||
btnNext.disabled = activeQuickEntryIndex === quickApprovalEntries.length - 1;
|
||||
btnNext.style.opacity = activeQuickEntryIndex === quickApprovalEntries.length - 1 ? '0.4' : '1';
|
||||
btnNext.style.cursor = activeQuickEntryIndex === quickApprovalEntries.length - 1 ? 'not-allowed' : 'pointer';
|
||||
|
||||
// Action button
|
||||
const actBtn = document.getElementById('qem-action-btn');
|
||||
if (entry.supervisor_approved) {
|
||||
actBtn.textContent = "Onayı Kaldır";
|
||||
actBtn.className = "px-5 py-2.5 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 {
|
||||
actBtn.textContent = "Günü Onayla";
|
||||
actBtn.className = "px-5 py-2.5 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 prevQuickEntry() {
|
||||
if (activeQuickEntryIndex > 0) {
|
||||
activeQuickEntryIndex--;
|
||||
renderQuickEntryDetails();
|
||||
}
|
||||
}
|
||||
|
||||
function nextQuickEntry() {
|
||||
if (activeQuickEntryIndex < quickApprovalEntries.length - 1) {
|
||||
activeQuickEntryIndex++;
|
||||
renderQuickEntryDetails();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleQuickEntryApproval() {
|
||||
if (activeQuickEntryIndex < 0 || isUpdatingQuickApproval) return;
|
||||
|
||||
const entry = quickApprovalEntries[activeQuickEntryIndex];
|
||||
const actBtn = document.getElementById('qem-action-btn');
|
||||
|
||||
isUpdatingQuickApproval = true;
|
||||
actBtn.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.id
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
entry.supervisor_approved = data.status;
|
||||
entry.supervisor_name = data.supervisor_name;
|
||||
|
||||
// If in unapproved filter, and we approved, we'll remove it after closing/switching, or we can handle it seamlessly
|
||||
// For modal, it's nice to either show updated status, or automatically move to next unapproved entry!
|
||||
|
||||
if (currentQuickFilter === 'unapproved' && data.status) {
|
||||
// Approved! Remove from list, decrement badge, and move to next or close if empty
|
||||
const oldIndex = activeQuickEntryIndex;
|
||||
quickApprovalEntries = quickApprovalEntries.filter(e => e.id !== entry.id);
|
||||
updateBadgeCount(quickApprovalEntries.length);
|
||||
|
||||
if (quickApprovalEntries.length === 0) {
|
||||
closeQuickEntryModal();
|
||||
document.getElementById('qa-list-grid').classList.add('hidden');
|
||||
document.getElementById('qa-empty').classList.remove('hidden');
|
||||
} else {
|
||||
// Keep the index clamped within the new list bounds
|
||||
activeQuickEntryIndex = Math.min(oldIndex, quickApprovalEntries.length - 1);
|
||||
renderQuickEntryDetails();
|
||||
renderQuickListGrid();
|
||||
}
|
||||
} else {
|
||||
// Just update UI inside modal & grid
|
||||
renderQuickEntryDetails();
|
||||
renderQuickListGrid();
|
||||
}
|
||||
} else {
|
||||
alert(data.message || 'Onay güncellenemedi.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert('İşlem gerçekleştirilemedi.');
|
||||
})
|
||||
.finally(() => {
|
||||
isUpdatingQuickApproval = false;
|
||||
actBtn.style.opacity = '1';
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>'"]/g,
|
||||
tag => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
"'": ''',
|
||||
'"': '"'
|
||||
}[tag] || tag)
|
||||
);
|
||||
}
|
||||
|
||||
// Handle ESC key for quick approval modal
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeQuickEntryModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=device-width, initial-scale=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>{{ $proposal->title }} - Trunçgil Teknoloji</title>
|
||||
|
||||
<!-- Google Fonts: Inter (Body), Outfit (Headings), Caveat (Signature Script) -->
|
||||
@@ -296,7 +297,68 @@
|
||||
display: block;
|
||||
}
|
||||
.mermaid-container.mermaid-active {
|
||||
box-shadow: 0 0 0 2px var(--accent-color, #4f46e5);
|
||||
box-shadow: 0 0 0 2px var(--accent-color, #ea580c);
|
||||
}
|
||||
|
||||
/* Mermaid Orange/Red Brand Theme Styling Overrides */
|
||||
.mermaid svg .task {
|
||||
fill: #ea580c !important;
|
||||
stroke: #c2410c !important;
|
||||
rx: 6px !important;
|
||||
ry: 6px !important;
|
||||
}
|
||||
.mermaid svg .task0, .mermaid svg .task2 {
|
||||
fill: #ea580c !important;
|
||||
stroke: #c2410c !important;
|
||||
}
|
||||
.mermaid svg .task1, .mermaid svg .task3 {
|
||||
fill: #ef4444 !important;
|
||||
stroke: #b91c1c !important;
|
||||
}
|
||||
.mermaid svg .taskText,
|
||||
.mermaid svg .taskTextOutsideRight,
|
||||
.mermaid svg .taskTextOutsideLeft {
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
.mermaid svg .taskText {
|
||||
fill: #ffffff !important;
|
||||
}
|
||||
.mermaid svg .taskTextOutsideRight,
|
||||
.mermaid svg .taskTextOutsideLeft {
|
||||
fill: #1e293b !important;
|
||||
}
|
||||
.dark .mermaid svg .taskTextOutsideRight,
|
||||
.dark .mermaid svg .taskTextOutsideLeft {
|
||||
fill: #f1f5f9 !important;
|
||||
}
|
||||
.mermaid svg .section0,
|
||||
.mermaid svg .section2 {
|
||||
fill: rgba(234, 88, 12, 0.08) !important;
|
||||
}
|
||||
.dark .mermaid svg .section0,
|
||||
.dark .mermaid svg .section2 {
|
||||
fill: rgba(234, 88, 12, 0.18) !important;
|
||||
}
|
||||
.mermaid svg .section1,
|
||||
.mermaid svg .section3 {
|
||||
fill: rgba(239, 68, 68, 0.08) !important;
|
||||
}
|
||||
.dark .mermaid svg .section1,
|
||||
.dark .mermaid svg .section3 {
|
||||
fill: rgba(239, 68, 68, 0.18) !important;
|
||||
}
|
||||
.mermaid svg .today {
|
||||
stroke: #ef4444 !important;
|
||||
stroke-width: 2px !important;
|
||||
}
|
||||
.mermaid svg .titleText {
|
||||
fill: #0f172a !important;
|
||||
font-weight: 700 !important;
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
}
|
||||
.dark .mermaid svg .titleText {
|
||||
fill: #f8fafc !important;
|
||||
}
|
||||
|
||||
/* Fullscreen overlay */
|
||||
@@ -411,27 +473,27 @@
|
||||
</style>
|
||||
|
||||
@php
|
||||
$accent = $proposal->meta['accent_color'] ?? 'indigo';
|
||||
$accent = $proposal->meta['accent_color'] ?? 'coral';
|
||||
$accentColor = match($accent) {
|
||||
'emerald' => '#059669',
|
||||
'cyberpunk' => '#ec4899',
|
||||
'coral' => '#ea580c',
|
||||
'amber' => '#d97706',
|
||||
default => '#4f46e5',
|
||||
default => '#ea580c',
|
||||
};
|
||||
$accentColorGlow = match($accent) {
|
||||
'emerald' => 'rgba(5, 150, 105, 0.2)',
|
||||
'cyberpunk' => 'rgba(236, 72, 153, 0.2)',
|
||||
'coral' => 'rgba(234, 88, 12, 0.2)',
|
||||
'amber' => 'rgba(217, 119, 6, 0.2)',
|
||||
default => 'rgba(79, 70, 229, 0.2)',
|
||||
default => 'rgba(234, 88, 12, 0.2)',
|
||||
};
|
||||
$themeClass = match($accent) {
|
||||
'emerald' => 'theme-emerald bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500 text-emerald-600 dark:text-emerald-400 border-emerald-500/20',
|
||||
'cyberpunk' => 'theme-rose bg-rose-500 hover:bg-rose-600 focus:ring-rose-400 text-rose-500 dark:text-rose-400 border-rose-500/20',
|
||||
'coral' => 'theme-orange bg-orange-600 hover:bg-orange-700 focus:ring-orange-500 text-orange-600 dark:text-orange-400 border-orange-500/20',
|
||||
'amber' => 'theme-amber bg-amber-600 hover:bg-amber-700 focus:ring-amber-500 text-amber-600 dark:text-amber-400 border-amber-500/20',
|
||||
default => 'theme-indigo bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500 text-indigo-600 dark:text-indigo-400 border-indigo-500/20',
|
||||
default => 'theme-orange bg-orange-600 hover:bg-orange-700 focus:ring-orange-500 text-orange-600 dark:text-orange-400 border-orange-500/20',
|
||||
};
|
||||
$gradient = match($accent) {
|
||||
'emerald' => 'from-emerald-600 to-teal-600 dark:from-emerald-500 dark:to-teal-500',
|
||||
@@ -966,10 +1028,39 @@
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: isDark ? 'dark' : 'default',
|
||||
theme: 'base',
|
||||
securityLevel: 'loose',
|
||||
themeVariables: {
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
darkMode: isDark,
|
||||
background: isDark ? '#0f172a' : '#ffffff',
|
||||
primaryColor: '#ea580c',
|
||||
primaryTextColor: '#ffffff',
|
||||
primaryBorderColor: '#c2410c',
|
||||
lineColor: '#ea580c',
|
||||
secondaryColor: '#ef4444',
|
||||
secondaryTextColor: '#ffffff',
|
||||
tertiaryColor: isDark ? '#1e293b' : '#fff7ed',
|
||||
tertiaryTextColor: isDark ? '#f8fafc' : '#9a3412',
|
||||
activeTaskBkgColor: '#ea580c',
|
||||
activeTaskBorderColor: '#c2410c',
|
||||
doneTaskBkgColor: '#f97316',
|
||||
doneTaskBorderColor: '#ea580c',
|
||||
critBkgColor: '#ef4444',
|
||||
critBorderColor: '#b91c1c',
|
||||
taskTextLightColor: '#ffffff',
|
||||
taskTextColor: '#ffffff',
|
||||
taskTextDarkColor: '#ffffff',
|
||||
taskTextOutsideColor: isDark ? '#e2e8f0' : '#1e293b',
|
||||
sectionBkgColor: isDark ? 'rgba(234, 88, 12, 0.15)' : 'rgba(234, 88, 12, 0.07)',
|
||||
sectionBkgColor2: isDark ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.07)',
|
||||
altSectionBkgColor: isDark ? 'rgba(234, 88, 12, 0.10)' : 'rgba(234, 88, 12, 0.04)',
|
||||
gridColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.08)',
|
||||
todayLineColor: '#ef4444',
|
||||
nodeBorder: '#ea580c',
|
||||
clusterBkg: isDark ? '#1e293b' : '#fff7ed',
|
||||
clusterBorder: '#f97316',
|
||||
titleColor: isDark ? '#f8fafc' : '#0f172a'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -986,20 +1077,18 @@
|
||||
const wrapper = document.getElementById('mermaid-wrapper-' + index);
|
||||
const canvas = document.getElementById('mermaid-canvas-' + index);
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (!wrapper || !canvas || !pz) return;
|
||||
if (!wrapper || !canvas) return;
|
||||
|
||||
wrapper.classList.add('mermaid-active');
|
||||
canvas.classList.add('mermaid-active');
|
||||
pz.enablePan();
|
||||
if (pz) pz.enablePan();
|
||||
}
|
||||
|
||||
function deactivateMermaid(index) {
|
||||
const wrapper = document.getElementById('mermaid-wrapper-' + index);
|
||||
const canvas = document.getElementById('mermaid-canvas-' + index);
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (wrapper) wrapper.classList.remove('mermaid-active');
|
||||
if (canvas) canvas.classList.remove('mermaid-active');
|
||||
if (pz) pz.disablePan();
|
||||
if (mermaidActiveIndex === index) mermaidActiveIndex = null;
|
||||
}
|
||||
|
||||
@@ -1041,25 +1130,19 @@
|
||||
wrapper.id = 'mermaid-wrapper-' + index;
|
||||
wrapper.dataset.mermaidSource = rawMermaid;
|
||||
|
||||
// Zoom Toolbar
|
||||
// Zoom Toolbar (Uzaklaştır [-] | 100% | Yakınlaştır [+] | Sıfırla/Sığdır | Tam Ekran)
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.className = 'mermaid-toolbar';
|
||||
toolbar.innerHTML = `
|
||||
<span class="mermaid-toolbar-label">Diyagram Görünümü</span>
|
||||
<span class="mermaid-toolbar-hint">Zoom ve kaydırma için tıklayın</span>
|
||||
<span class="mermaid-toolbar-label">DİYAGRAM GÖRÜNÜMÜ</span>
|
||||
<span class="mermaid-toolbar-hint">Zoom ve sürükleme için tıklayın</span>
|
||||
<div class="mermaid-toolbar-controls">
|
||||
<button class="mermaid-zoom-btn" title="Yakınlaştır (Zoom In)" onclick="mermaidZoomIn(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<span class="mermaid-zoom-pct" id="mermaid-pct-${index}">100%</span>
|
||||
<button class="mermaid-zoom-btn" title="Uzaklaştır (Zoom Out)" onclick="mermaidZoomOut(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Tam Boyut (1:1)" onclick="mermaidZoomReset(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Tam Ekran" onclick="mermaidFullscreen(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
|
||||
<span class="mermaid-zoom-pct" id="mermaid-pct-${index}">100%</span>
|
||||
<button class="mermaid-zoom-btn" title="Yakınlaştır (Zoom In)" onclick="mermaidZoomIn(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -1070,7 +1153,7 @@
|
||||
canvas.id = 'mermaid-canvas-' + index;
|
||||
|
||||
const graphDiv = document.createElement('div');
|
||||
graphDiv.className = 'mermaid w-full';
|
||||
graphDiv.className = 'mermaid w-full h-full';
|
||||
graphDiv.id = 'mermaid-graph-' + index;
|
||||
graphDiv.textContent = rawMermaid;
|
||||
|
||||
@@ -1088,19 +1171,21 @@
|
||||
<div class="mermaid-fullscreen-inner" id="mermaid-fs-inner">
|
||||
<div class="mermaid-toolbar" style="border-radius:20px 20px 0 0;">
|
||||
<span class="mermaid-toolbar-label">Tam Ekran Görünüm</span>
|
||||
<button class="mermaid-zoom-btn" title="Yakınlaştır" onclick="mermaidFsZoom('in')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<span class="mermaid-zoom-pct" id="mermaid-fs-pct">100%</span>
|
||||
<button class="mermaid-zoom-btn" title="Uzaklaştır" onclick="mermaidFsZoom('out')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="1:1 Sıfırla" onclick="mermaidFsZoom('reset')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Kapat" onclick="closeMermaidFullscreen()" style="background:#ef4444;border-color:#ef4444;color:#fff;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
<div class="mermaid-toolbar-controls">
|
||||
<button class="mermaid-zoom-btn" title="Uzaklaştır" onclick="mermaidFsZoom('out')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<span class="mermaid-zoom-pct" id="mermaid-fs-pct">100%</span>
|
||||
<button class="mermaid-zoom-btn" title="Yakınlaştır" onclick="mermaidFsZoom('in')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Sığdır / Sıfırla" onclick="mermaidFsZoom('reset')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Kapat" onclick="closeMermaidFullscreen()" style="background:#ef4444;border-color:#ef4444;color:#fff;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mermaid-fullscreen-canvas" id="mermaid-fs-canvas"></div>
|
||||
</div>
|
||||
@@ -1117,10 +1202,7 @@
|
||||
// Run mermaid parser
|
||||
setTimeout(() => {
|
||||
mermaid.run().then(() => {
|
||||
// Re-init lucide icons inside alerts after marked rendering completes
|
||||
lucide.createIcons();
|
||||
|
||||
// Initialize svg-pan-zoom for each mermaid diagram
|
||||
codeElements.forEach((_, index) => {
|
||||
initMermaidZoom(index);
|
||||
});
|
||||
@@ -1136,48 +1218,50 @@
|
||||
const svg = canvas.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
// Ensure SVG has an ID for svg-pan-zoom
|
||||
if (!svg.id) svg.id = 'mermaid-svg-' + index;
|
||||
|
||||
// Compute a good canvas height from SVG viewBox aspect ratio
|
||||
const canvasW = canvas.clientWidth || canvas.offsetWidth || 800;
|
||||
let canvasH = 520;
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.style.width = '100%';
|
||||
svg.style.height = '100%';
|
||||
|
||||
// Compute canvas height from SVG viewBox aspect ratio
|
||||
let canvasH = 500;
|
||||
const vb = svg.getAttribute('viewBox');
|
||||
if (vb) {
|
||||
const parts = vb.split(/[\s,]+/);
|
||||
if (parts.length >= 4) {
|
||||
const svgW = parseFloat(parts[2]);
|
||||
const svgH = parseFloat(parts[3]);
|
||||
const canvasW = canvas.clientWidth || 800;
|
||||
if (svgW > 0 && svgH > 0) {
|
||||
canvasH = Math.max(400, Math.min(Math.round(svgH * (canvasW / svgW)), 900));
|
||||
canvasH = Math.max(380, Math.min(Math.round(svgH * (canvasW / svgW)), 800));
|
||||
}
|
||||
}
|
||||
}
|
||||
canvas.style.height = canvasH + 'px';
|
||||
|
||||
// svg-pan-zoom needs explicit width/height on the SVG element
|
||||
svg.setAttribute('width', canvasW);
|
||||
svg.setAttribute('height', canvasH);
|
||||
svg.style.width = canvasW + 'px';
|
||||
svg.style.height = canvasH + 'px';
|
||||
|
||||
try {
|
||||
if (mermaidZoomInstances[index]) {
|
||||
try { mermaidZoomInstances[index].destroy(); } catch(e) {}
|
||||
}
|
||||
|
||||
const panZoom = svgPanZoom('#' + svg.id, {
|
||||
zoomEnabled: true,
|
||||
panEnabled: false,
|
||||
panEnabled: true,
|
||||
controlIconsEnabled: false,
|
||||
fit: true,
|
||||
center: true,
|
||||
minZoom: 0.3,
|
||||
maxZoom: 8,
|
||||
zoomScaleSensitivity: 0.3,
|
||||
maxZoom: 10,
|
||||
zoomScaleSensitivity: 0.25,
|
||||
mouseWheelZoomEnabled: false,
|
||||
onZoom: (zoom) => {
|
||||
const pctEl = document.getElementById('mermaid-pct-' + index);
|
||||
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
|
||||
updatePct(index, zoom);
|
||||
}
|
||||
});
|
||||
mermaidZoomInstances[index] = panZoom;
|
||||
updatePct(index, panZoom.getZoom());
|
||||
setupMermaidActivation(index);
|
||||
} catch(e) {
|
||||
console.warn('svg-pan-zoom init failed for mermaid-' + index, e);
|
||||
@@ -1198,7 +1282,12 @@
|
||||
function mermaidZoomReset(index) {
|
||||
setMermaidActive(index);
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (pz) { pz.resetZoom(); pz.center(); updatePct(index, pz.getZoom()); }
|
||||
if (pz) {
|
||||
pz.resetZoom();
|
||||
pz.fit();
|
||||
pz.center();
|
||||
updatePct(index, pz.getZoom());
|
||||
}
|
||||
}
|
||||
function updatePct(index, zoom) {
|
||||
const pctEl = document.getElementById('mermaid-pct-' + index);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -134,6 +136,7 @@ Route::get('/stajyer/admin/panel', [\App\Http\Controllers\CareerController::clas
|
||||
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::get('/stajyer/admin/quick-approval-entries', [\App\Http\Controllers\CareerController::class, 'getQuickApprovalEntries'])->name('intern.admin.quick-approval-entries');
|
||||
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');
|
||||
|
||||
@@ -171,6 +174,18 @@ Route::get('/sitemap.xml', [\App\Http\Controllers\SitemapController::class, 'ind
|
||||
Route::get('/teklif/{slug}', [\App\Http\Controllers\ProposalController::class, 'show'])->name('proposals.show');
|
||||
Route::post('/teklif/{slug}/action', [\App\Http\Controllers\ProposalController::class, 'action'])->name('proposals.action');
|
||||
|
||||
// Project Tracking Client Portal & Web Admin Management
|
||||
Route::get('/proje-takip/{slug}', [\App\Http\Controllers\ProjectController::class, 'show'])->name('projects.show');
|
||||
Route::post('/proje-takip/{slug}/verify', [\App\Http\Controllers\ProjectController::class, 'verify'])->name('projects.verify');
|
||||
Route::post('/proje-takip/{slug}/admin/module-status', [\App\Http\Controllers\ProjectController::class, 'updateModuleStatus'])->name('projects.admin.module-status');
|
||||
Route::post('/proje-takip/{slug}/admin/task-status', [\App\Http\Controllers\ProjectController::class, 'updateTaskStatus'])->name('projects.admin.task-status');
|
||||
Route::post('/proje-takip/{slug}/admin/add-task', [\App\Http\Controllers\ProjectController::class, 'addTask'])->name('projects.admin.add-task');
|
||||
Route::post('/proje-takip/{slug}/admin/delete-task', [\App\Http\Controllers\ProjectController::class, 'deleteTask'])->name('projects.admin.delete-task');
|
||||
Route::post('/proje-takip/{slug}/admin/add-update', [\App\Http\Controllers\ProjectController::class, 'addUpdate'])->name('projects.admin.add-update');
|
||||
Route::post('/proje-takip/{slug}/admin/delete-update', [\App\Http\Controllers\ProjectController::class, 'deleteUpdate'])->name('projects.admin.delete-update');
|
||||
Route::post('/proje-takip/{slug}/admin/recalculate', [\App\Http\Controllers\ProjectController::class, 'recalculate'])->name('projects.admin.recalculate');
|
||||
Route::post('/proje-takip/{slug}/admin/toggle-mode', [\App\Http\Controllers\ProjectController::class, 'toggleAdminMode'])->name('projects.admin.toggle-mode');
|
||||
|
||||
// Privacy Policy Alternatives
|
||||
Route::get('/privacy-policy', function () {
|
||||
return app(PageController::class)->show('privacy');
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
$json = file_get_contents(__DIR__ . '/intern_data.json');
|
||||
$interns = json_decode($json, true);
|
||||
|
||||
$report = "";
|
||||
foreach ($interns as $intern) {
|
||||
$report .= "========================================\n";
|
||||
$report .= "NAME: " . $intern['name'] . "\n";
|
||||
$report .= "Email: " . $intern['email'] . "\n";
|
||||
$report .= "GitHub Repo: " . ($intern['github_repo'] ?: 'NONE') . "\n";
|
||||
$report .= "Start: " . $intern['internship_start_date'] . " | End: " . $intern['internship_end_date'] . "\n";
|
||||
$report .= "Total Journal Entries: " . count($intern['journal_entries']) . "\n";
|
||||
|
||||
foreach ($intern['journal_entries'] as $entry) {
|
||||
$report .= "--- Date: " . $entry['date'] . " | Day: " . $entry['day_number'] . " | Retroactive: " . ($entry['is_retroactive'] ? 'YES' : 'NO') . " | Approved: " . ($entry['supervisor_approved'] ? 'YES' : 'NO') . "\n";
|
||||
$report .= strip_tags($entry['content']) . "\n";
|
||||
}
|
||||
$report .= "\n\n";
|
||||
}
|
||||
|
||||
file_put_contents(__DIR__ . '/detailed_report.txt', $report);
|
||||
echo "Written to scratch/detailed_report.txt\n";
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
$app = require_once __DIR__ . '/../bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
$interns = \App\Models\CareerApplication::where('status', 'accepted')
|
||||
->with(['journalEntries' => function($q) {
|
||||
$q->orderBy('date', 'asc');
|
||||
}])
|
||||
->get();
|
||||
|
||||
$data = [];
|
||||
foreach ($interns as $intern) {
|
||||
$entries = [];
|
||||
foreach ($intern->journalEntries as $entry) {
|
||||
$entries[] = [
|
||||
'day_number' => $entry->day_number,
|
||||
'date' => $entry->date,
|
||||
'content' => $entry->content,
|
||||
'is_retroactive' => $entry->is_retroactive,
|
||||
'supervisor_approved' => $entry->supervisor_approved,
|
||||
'unit_approved' => $entry->unit_approved,
|
||||
];
|
||||
}
|
||||
$data[] = [
|
||||
'id' => $intern->id,
|
||||
'name' => $intern->name,
|
||||
'email' => $intern->email,
|
||||
'github_username' => $intern->github_username,
|
||||
'github_repo' => $intern->github_repo,
|
||||
'internship_start_date' => $intern->internship_start_date,
|
||||
'internship_end_date' => $intern->internship_end_date,
|
||||
'journal_entries' => $entries,
|
||||
];
|
||||
}
|
||||
|
||||
file_put_contents(__DIR__ . '/intern_data.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
echo "Done! Saved to scratch/intern_data.json\n";
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
$json = file_get_contents(__DIR__ . '/intern_data.json');
|
||||
$interns = json_decode($json, true);
|
||||
|
||||
foreach ($interns as $intern) {
|
||||
echo "========================================\n";
|
||||
echo "NAME: " . $intern['name'] . "\n";
|
||||
echo "Email: " . $intern['email'] . "\n";
|
||||
echo "GitHub Repo: " . ($intern['github_repo'] ?: 'NONE') . "\n";
|
||||
echo "Start: " . $intern['internship_start_date'] . " | End: " . $intern['internship_end_date'] . "\n";
|
||||
echo "Total Journal Entries: " . count($intern['journal_entries']) . "\n";
|
||||
|
||||
// Group journal entries by week or get recent ones
|
||||
// Let's filter entries for "this week" (2026-07-10 to 2026-07-17)
|
||||
$this_week_entries = [];
|
||||
foreach ($intern['journal_entries'] as $entry) {
|
||||
if ($entry['date'] >= '2026-07-10' && $entry['date'] <= '2026-07-17') {
|
||||
$this_week_entries[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
echo "This Week's Entries (" . count($this_week_entries) . "):\n";
|
||||
foreach ($this_week_entries as $entry) {
|
||||
echo " - Date: " . $entry['date'] . " | Day: " . $entry['day_number'] . " | Retroactive: " . ($entry['is_retroactive'] ? 'YES' : 'NO') . "\n";
|
||||
// Show first 200 chars of content
|
||||
$text = trim(strip_tags($entry['content']));
|
||||
$text = str_replace(["\r", "\n", "\t"], ' ', $text);
|
||||
$text = preg_replace('/\s+/', ' ', $text);
|
||||
echo " Content: " . mb_substr($text, 0, 150) . "...\n";
|
||||
}
|
||||
|
||||
// Also list all entries briefly to understand overall progress
|
||||
echo "All Entries dates: ";
|
||||
$dates = [];
|
||||
foreach ($intern['journal_entries'] as $entry) {
|
||||
$dates[] = $entry['date'] . ($entry['is_retroactive'] ? '(R)' : '');
|
||||
}
|
||||
echo implode(', ', $dates) . "\n";
|
||||
}
|
||||
Reference in New Issue
Block a user