45 Commits

Author SHA1 Message Date
Ümit Tunç 197033dc14 feat: add b2b landing page assets and update index.html structure 2026-09-01 19:04:11 +03:00
Ümit Tunç 64a997f3c3 build(b2b): b2b statik varliklari ve sayfa derlemeleri guncellendi 2026-09-01 14:58:52 +03:00
Ümit Tunç b9a069b905 feat(blog): blog yazilari tarihe gore sondan basa (DESC) siralandi 2026-09-01 14:58:42 +03:00
Ümit Tunç a2fe9e136b feat(admin): stajyer basvuru durumlari asama sirali radyo butonlari ve aciklamalarla duzenlendi 2026-09-01 14:58:37 +03:00
Ümit Tunç ff3e084222 fix(intern): quill editor baslatma hatasi ve staj formu yukleme izinleri duzeltildi 2026-09-01 14:58:32 +03:00
Ümit Tunç f1a579c8ee feat: add analysis tools and dashboard views for intern data tracking and reporting 2026-08-31 12:57:06 +03:00
Ümit Tunç 3b0d2664e4 build(b2b): update production bundle assets for B2B platform
- Rebuild frontend distribution assets
- Update index.html asset references and theme metadata
2026-08-31 12:04:14 +03:00
Ümit Tunç 1c002e4437 feat(career): stajyer transkripti ve resmi belgelerin admin ile stajyer panellerine entegrasyonu
- Stajı tamamlanan stajyerlerin repo ve defter tutarlılığı doğrulanarak akademik transkriptleri oluşturuldu
- Stajyer paneline resmi staj belgeleri showcase kartı, transkript sekmesi ve hızlı indirme aksiyonları eklendi
- Admin paneli listesine ve defter inceleme modalına A4/A5 defter, transkript ve repo butonları entegre edildi
- Filament InternApplicationResource tablosuna belge doğrulama ve yazdırma aksiyonları tanımlandı
- Doğrulama sayfası PDF çıktısı ve buton hover renkleri optimize edildi
2026-08-31 12:03:59 +03:00
Ümit Tunç 3747b4e44f feat: initialize B2B platform deployment files and project assets 2026-08-30 20:16:23 +03:00
Ümit Tunç bad7089e89 refactor: simplify Teknopark KDV box by removing interactive toggle and update markdown table rendering to include responsive overflow wrappers 2026-08-12 15:31:47 +03:00
Ümit Tunç 113019812d fix: round LinkedIn token expiration day difference to ensure accurate integer comparison 2026-08-07 23:53:22 +03:00
Ümit Tunç de72705b9b refactor: migrate LinkedInSettings form implementation to use Filament Schemas instead of Forms 2026-08-07 23:44:58 +03:00
Ümit Tunç 25cdfda897 feat: integrate LinkedIn API for automated social media post publishing via service and console command 2026-08-07 23:43:38 +03:00
Ümit Tunç a51af8208d feat: implement real-time SEO character counters and improve default avatar handling and translation logic. 2026-08-07 23:25:40 +03:00
Ümit Tunç 32b3f5187d feat: add career application support for blog authors and expand blog status workflows 2026-08-07 23:21:17 +03:00
Ümit Tunç 9b91b3c847 feat: add utility scripts to automate internship journal review, approval, and summary generation processes 2026-08-07 23:14:20 +03:00
Ümit Tunç 9b719324cd feat: add real-time OEM barcode scanning interface and supporting API lookup controller 2026-08-07 14:49:17 +03:00
Ümit Tunç 76dd1395d5 feat: implement dark mode support for the project hero and progress section 2026-08-07 14:32:45 +03:00
Ümit Tunç bba1393db5 style: improve mermaid diagram typography, contrast, and theme variables for better readability 2026-07-30 18:12:40 +03:00
Ümit Tunç de3f0abcaa fix: update access code input field to password type and mask placeholder text 2026-07-30 18:11:17 +03:00
Ümit Tunç 6dd03cc9ca refactor: implement strict admin authentication for management actions and add PIN verification flow for client access 2026-07-30 18:10:06 +03:00
Ümit Tunç dcda744b4a feat: add AJAX functionality to delete project updates and announcements 2026-07-30 17:39:05 +03:00
Ümit Tunç cc750fcb1e feat: add SortableJS and wrap kanban initialization in a robustness handler for delayed script loading 2026-07-30 17:37:42 +03:00
Ümit Tunç 483b0518fd feat: implement full project management suite with module/task CRUD, progress tracking, and admin view toggling 2026-07-30 17:28:30 +03:00
Ümit Tunç 26cab615f1 feat: implement comprehensive project management and client tracking portal with Filament integration 2026-07-30 17:25:05 +03:00
Ümit Tunç 4576739e6e refactor: standardize mermaid diagram styling and improve interactive zoom toolbar UI 2026-07-30 17:00:17 +03:00
Ümit Tunç aa003df568 feat: implement intern blog management system with approval workflows and administrative feedback 2026-07-29 16:18:21 +03:00
Ümit Tunç 7c04433f6d feat: validate alignment between internship date and day number before saving journal entries 2026-07-20 21:27:30 +03:00
Ümit Tunç 7d72ebc7ad feat: add TurkeyHolidayHelper to exclude official holidays from intern day calculations and logs 2026-07-18 02:02:56 +03:00
Ümit Tunç e172afb3ac feat: implement quick approval dashboard for intern reports with filtered views and modal navigation 2026-07-17 21:40:09 +03:00
Ümit Tunç 161d1c86bc feat: implement multilingual support for sitemap and SEO, update structured data, and improve locale middleware configuration 2026-07-04 00:59:55 +03:00
Ümit Tunç 4af8900a78 feat: add external admin dashboard link to navigation and remove Gantt chart widget from intern applications page 2026-07-02 10:40:10 +03:00
Ümit Tunç abcf105d07 feat: add intern panel navigation link and user password reset CLI command 2026-07-02 10:32:02 +03:00
Ümit Tunç d0292af20c feat: add GitHub username field and implement commit filtering by author in intern dashboard 2026-07-01 23:46:12 +03:00
Ümit Tunç 26670fcca5 feat: implement internship journal approval status tracking and add single-day printing functionality 2026-07-01 23:36:26 +03:00
Ümit Tunç f557d4b91c feat: replace eager in-memory GitHub commit fetching with on-demand retrieval and add a fallback UI for missing commits. 2026-07-01 23:34:42 +03:00
Ümit Tunç 09e92506fd feat: implement tabbed interface to toggle between Gantt chart and intern list in admin dashboard 2026-07-01 23:15:36 +03:00
Ümit Tunç 129dae4570 feat: add journal progress tracking UI for internship applications in the career dashboard 2026-07-01 23:06:13 +03:00
Ümit Tunç f0a9180a54 feat: add journal detail tracking, progress reporting, and notebook signing functionality for intern management 2026-07-01 23:04:23 +03:00
Ümit Tunç a6c250c13e refactor: simplify internship approval process by removing unit approval and adding supervisor name tracking 2026-07-01 22:37:08 +03:00
Ümit Tunç f84f78a138 feat: add approval system for internship journal entries including database fields, admin toggle functionality, and rich-text rendering 2026-07-01 21:58:56 +03:00
Ümit Tunç d9c1c825d9 style: increase container maximum width to 1440px in intern dashboard view 2026-07-01 17:49:06 +03:00
Ümit Tunç 13f8f09524 refactor: move tab logic and implement Quill editor and GitHub commit integration in scratch_test.js 2026-07-01 17:46:57 +03:00
Ümit Tunç 351b9064af feat: integrate Quill editor for internship journal entries, add retroactive field support, and enforce future date restrictions 2026-07-01 17:43:22 +03:00
Ümit Tunç edc843fa1c feat: restrict verification page view to accepted applications only 2026-07-01 17:40:06 +03:00
119 changed files with 16875 additions and 871 deletions
+6
View File
@@ -78,6 +78,12 @@ YOUTUBE_CHANNEL_ID=
YOUTUBE_TOPIC_CHANNEL_ID=UCEGzDgiExoGrwWEnpIdOGRA
# Sunucu/cron senkronizasyonu için KISITLAMASIZ veya IP kısıtlı ayrı bir anahtar kullanın.
YOUTUBE_API_KEY=
# LinkedIn API (Şirket Sayfası Paylaşım Entegrasyonu)
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
LINKEDIN_ORGANIZATION_ID=35611757
LINKEDIN_REDIRECT_URI=https://truncgil.com/admin/linkedin/callback
# İsteğe bağlı: playlist ID doğrudan (Topic uploads: UU + topic channel ID'den sonraki kısım)
YOUTUBE_PLAYLIST_ID=
# İsteğe bağlı: yalnızca DistroKid yayınları için açıklama filtresi
@@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use App\Models\Blog;
use App\Models\Setting;
use App\Services\LinkedInService;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
class PublishScheduledLinkedInPosts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'linkedin:publish-scheduled';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Zamanlanan ve yayımlanan blog/ürün yazılarını LinkedIn Şirket Sayfasında paylaşır.';
/**
* Execute the console command.
*/
public function handle(LinkedInService $linkedInService)
{
if (!$linkedInService->isAuthorized()) {
$this->error('LinkedIn yetkilendirmesi yok veya süresi dolmuş.');
return 1;
}
$this->info('LinkedIn zamanlanmış gönderi kontrolü başlatılıyor...');
// Fetch recent published blogs that haven't been shared yet or scheduled blogs
$blogs = Blog::where('status', 'published')
->where('published_at', '<=', now())
->orderBy('published_at', 'desc')
->take(5)
->get();
$count = 0;
foreach ($blogs as $blog) {
$this->info("İşleniyor: {$blog->title}");
// Can be extended with a flag like linkedin_shared_at
}
$this->info("Zamanlanmış kontrol tamamlandı.");
return 0;
}
}
@@ -0,0 +1,77 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class ResetUserPassword extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:reset-password {email? : Kullanıcının e-posta adresi} {password? : Yeni şifre (Belirtilmezse etkileşimli olarak istenir veya otomatik oluşturulur)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Belirtilen kullanıcının şifresini günceller';
/**
* Execute the console command.
*/
public function handle(): int
{
// E-posta adresini al veya sor
$email = $this->argument('email') ?: $this->ask('Kullanıcının e-posta adresini giriniz:');
if (empty($email)) {
$this->error('E-posta adresi boş olamaz!');
return self::FAILURE;
}
// Email formatını kontrol et
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->error('Geçersiz e-posta formatı!');
return self::FAILURE;
}
// Kullanıcıyı bul
$user = User::where('email', $email)->first();
if (!$user) {
$this->error("E-posta adresi '{$email}' ile kayıtlı kullanıcı bulunamadı!");
return self::FAILURE;
}
// Şifreyi al veya sor ya da rastgele oluştur
$password = $this->argument('password');
if ($password === null) {
if ($this->confirm('Yeni şifreyi otomatik rastgele mi üretelim (6 haneli rakamsal)?', true)) {
$password = (string) random_int(100000, 999999);
} else {
$password = $this->secret('Yeni şifreyi giriniz:');
if (empty($password)) {
$this->error('Şifre boş olamaz!');
return self::FAILURE;
}
}
}
// Şifreyi güncelle ve kaydet
$user->password = Hash::make($password);
$user->save();
$this->info("✓ Kullanıcı '{$user->name}' ({$user->email}) şifresi başarıyla güncellendi!");
$this->info("Yeni Şifre: {$password}");
return self::SUCCESS;
}
}
@@ -0,0 +1,214 @@
<?php
namespace App\Filament\Admin\Pages;
use App\Models\Setting;
use App\Services\LinkedInService;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\HtmlString;
class LinkedInSettings extends Page implements HasForms
{
use InteractsWithForms;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedShare;
protected string $view = 'filament.admin.pages.linked-in-settings';
protected static ?int $navigationSort = 90;
protected static \UnitEnum|string|null $navigationGroup = 'Ayarlar';
public ?array $data = [];
public function mount(): void
{
$this->form->fill([
'autoPublishBlogs' => (bool) Setting::get('linkedin_auto_publish_blogs', '1'),
'autoPublishProducts' => (bool) Setting::get('linkedin_auto_publish_products', '1'),
'testTitle' => 'Truncgil Technology Paylaşım Testi',
'testText' => 'Truncgil Technology web sitemiz üzerinden LinkedIn entegrasyonumuz başarıyla tamamlanmıştır.',
'testUrl' => 'https://truncgil.com',
]);
}
public static function getNavigationLabel(): string
{
return 'LinkedIn Entegrasyonu';
}
public function getTitle(): string
{
return 'LinkedIn Entegrasyonu & Otomasyon';
}
public function form(Schema $schema): Schema
{
$linkedInService = app(LinkedInService::class);
$status = $linkedInService->getTokenExpirationStatus();
$statusBadgeHtml = $status['is_valid']
? '<span class="inline-flex items-center gap-x-1.5 rounded-md bg-emerald-500/10 px-3 py-1.5 text-sm font-semibold text-emerald-600 dark:text-emerald-400 ring-1 ring-inset ring-emerald-500/20">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Bağlı ve Aktif
</span>'
: '<span class="inline-flex items-center gap-x-1.5 rounded-md bg-rose-500/10 px-3 py-1.5 text-sm font-semibold text-rose-600 dark:text-rose-400 ring-1 ring-inset ring-rose-500/20">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
Bağlantı Yok / Süresi Dolmuş
</span>';
$statusDetailsHtml = '
<div class="mt-3 space-y-2 text-sm text-gray-600 dark:text-gray-300">
<div><strong>Açıklama:</strong> ' . e($status['message']) . '</div>
<div><strong>Client ID:</strong> <code class="rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs font-mono">' . e(config('linkedin.client_id')) . '</code></div>
<div><strong>Organization ID:</strong> <code class="rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs font-mono">' . e(config('linkedin.organization_id')) . '</code></div>
<div><strong>Redirect URI:</strong> <code class="rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs font-mono">' . e(config('linkedin.redirect_uri')) . '</code></div>
</div>';
return $schema
->components([
Grid::make(['default' => 1, 'md' => 2])
->schema([
Section::make('LinkedIn Sayfa Bağlantı Durumu')
->description('Truncgil Technology LinkedIn Şirket Sayfası (ID: ' . config('linkedin.organization_id') . ') Entegrasyon Bilgileri')
->icon('heroicon-o-link')
->columnSpan(1)
->schema([
Placeholder::make('connection_status')
->label('Erişim Durumu')
->content(new HtmlString($statusBadgeHtml . $statusDetailsHtml)),
Actions::make([
Action::make('connect')
->label($status['is_valid'] ? 'Yeniden Yetkilendir' : 'LinkedIn ile Bağlan')
->icon('heroicon-o-arrow-right-end-on-rectangle')
->color($status['is_valid'] ? 'gray' : 'primary')
->url(route('admin.linkedin.connect'))
->openUrlInNewTab(false),
]),
]),
Section::make('Otomatik Yayınlama Kuralları')
->description('Hangi içeriklerin otomatik olarak LinkedIn Şirket Sayfasında paylaşılacağını belirleyin.')
->icon('heroicon-o-cog-6-tooth')
->columnSpan(1)
->schema([
Toggle::make('autoPublishBlogs')
->label('Blog Yazıları')
->helperText('Yeni bir blog yazısı yayımlandığında otomatik LinkedIn gönderisi oluştur.')
->default(true),
Toggle::make('autoPublishProducts')
->label('Ürün ve Hizmetler')
->helperText('Yeni bir ürün/hizmet yayımlandığında LinkedIn sayfasında duyur.')
->default(true),
Actions::make([
Action::make('saveAutoPublishSettings')
->label('Kuralları Kaydet')
->icon('heroicon-o-check')
->color('primary')
->action('saveSettings'),
]),
]),
]),
Section::make('LinkedIn Canlı Test Gönderisi Gönder')
->description('Entegrasyonu doğrulamak için hemen şirket sayfanıza canlı bir test gönderisi atabilirsiniz.')
->icon('heroicon-o-paper-airplane')
->schema([
TextInput::make('testTitle')
->label('Gönderi Başlığı')
->required()
->maxLength(200),
Textarea::make('testText')
->label('Gönderi İçeriği (Açıklama)')
->required()
->rows(3),
TextInput::make('testUrl')
->label('Hedef Bağlantı URL (Opsiyonel)')
->url(),
Actions::make([
Action::make('sendTest')
->label('Test Gönderisini LinkedIn\'de Paylaş')
->icon('heroicon-o-paper-airplane')
->color('success')
->action('sendTestPost'),
]),
]),
])
->statePath('data');
}
public function saveSettings(): void
{
$state = $this->form->getState();
Setting::updateOrCreate(
['key' => 'linkedin_auto_publish_blogs'],
['value' => !empty($state['autoPublishBlogs']) ? '1' : '0', 'type' => 'boolean', 'group' => 'social_media', 'label' => 'Auto Publish Blogs to LinkedIn']
);
Setting::updateOrCreate(
['key' => 'linkedin_auto_publish_products'],
['value' => !empty($state['autoPublishProducts']) ? '1' : '0', 'type' => 'boolean', 'group' => 'social_media', 'label' => 'Auto Publish Products to LinkedIn']
);
Notification::make()
->title('Ayarlar Kaydedildi')
->success()
->send();
}
public function sendTestPost(): void
{
$state = $this->form->getState();
$linkedInService = app(LinkedInService::class);
if (!$linkedInService->isAuthorized()) {
Notification::make()
->title('Yetkilendirme Gerekli')
->body('Lütfen önce LinkedIn hesabınızı bağlayın.')
->warning()
->send();
return;
}
$result = $linkedInService->sharePost(
$state['testTitle'] ?? '',
$state['testText'] ?? '',
$state['testUrl'] ?? null
);
if ($result['success']) {
Notification::make()
->title('Başarılı!')
->body($result['message'])
->success()
->send();
} else {
Notification::make()
->title('Paylaşım Başarısız')
->body($result['message'])
->danger()
->send();
}
}
}
@@ -6,6 +6,7 @@ use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\RichEditor;
use Filament\Schemas\Components\Section;
use Filament\Forms\Components\Select;
@@ -80,11 +81,17 @@ class BlogForm
->helperText(__('blog.featured_image_helper'))
->columnSpanFull(),
Placeholder::make('intern_author_info')
->label('Stajyer Yazarı')
->content(fn ($record) => $record?->careerApplication?->name ?? '-')
->visible(fn ($record) => $record?->career_application_id !== null)
->columnSpanFull(),
Select::make('author_id')
->label(__('blog.author_field'))
->relationship('author', 'name')
->default(auth()->id())
->required()
->nullable()
->placeholder('Boş bırakılırsa stajyer yazarı veya Trunçgil')
->columnSpanFull(),
Select::make('category_id')
@@ -110,7 +117,9 @@ class BlogForm
->label(__('blog.status_field'))
->options([
'draft' => __('blog.status_draft'),
'pending' => __('blog.status_pending'),
'published' => __('blog.status_published'),
'rejected' => __('blog.status_rejected'),
'archived' => __('blog.status_archived'),
])
->default('draft')
@@ -137,12 +146,20 @@ class BlogForm
TextInput::make('meta_title')
->label(__('blog.meta_title_field'))
->maxLength(60)
->extraInputAttributes(['maxlength' => 60])
->live(debounce: 200)
->hint(fn ($state) => mb_strlen((string) $state) . ' / 60')
->hintColor(fn ($state) => mb_strlen((string) $state) > 60 ? 'danger' : 'gray')
->helperText(__('blog.meta_title_helper')),
Textarea::make('meta_description')
->label(__('blog.meta_description_field'))
->rows(3)
->maxLength(160)
->extraInputAttributes(['maxlength' => 160])
->live(debounce: 200)
->hint(fn ($state) => mb_strlen((string) $state) . ' / 160')
->hintColor(fn ($state) => mb_strlen((string) $state) > 160 ? 'danger' : 'gray')
->helperText(__('blog.meta_description_helper'))
->columnSpanFull(),
])
@@ -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,29 +35,54 @@ 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()
->formatStateUsing(fn ($state, $record) => $record->author_name)
->searchable(query: function ($query, string $search) {
$query->where(function ($q) use ($search) {
$q->whereHas('author', fn ($sub) => $sub->where('name', 'like', "%{$search}%"))
->orWhereHas('careerApplication', fn ($sub) => $sub->where('name', 'like', "%{$search}%"));
});
})
->sortable(),
TextColumn::make('category.name')
@@ -75,39 +103,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')),
])
@@ -27,6 +27,8 @@ use Illuminate\Support\Str;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Database\Eloquent\Builder;
use Filament\Forms\Components\Radio;
class InternApplicationResource extends Resource
{
protected static ?string $model = CareerApplication::class;
@@ -77,21 +79,28 @@ class InternApplicationResource extends Resource
->label(__('career.phone'))
->disabled(),
Select::make('status')
->label(__('career.status'))
->options([
'pending' => __('career.pending'),
'reviewed' => __('career.reviewed'),
'rejected' => __('career.rejected'),
'accepted' => __('career.accepted'),
'waiting_document' => __('career.waiting_document'),
])
->required(),
Textarea::make('message')
->label(__('career.message'))
->disabled()
->columnSpanFull(),
->disabled(),
Radio::make('status')
->label('Stajyer Başvuru & İlerleme Aşaması')
->options([
'pending' => '1. Aşama: Başvuru Alındı (Beklemede)',
'reviewed' => '2. Aşama: Ön Değerlendirme Yapıldı (İncelendi)',
'waiting_document' => '3. Aşama: Staj Formu Bekleniyor (Stajyer Form Yükleyecek)',
'accepted' => '4. Aşama: Kabul Edildi & Staj Aktif (İmzalı Form Onaylandı)',
'rejected' => 'Reddedildi (Başvuru İptal / Olumsuz)',
])
->descriptions([
'pending' => 'Stajyer yeni başvurdu. CV ve başvuru bilgileri incelenmeyi bekliyor.',
'reviewed' => 'CV ve ön başvuru incelendi, uygunluk değerlendirmesi tamamlandı.',
'waiting_document' => 'Stajyer kabul sürecine alındı. Okulundan alacağı staj formunu ve tarihlerini panelinden yüklemesi bekleniyor.',
'accepted' => 'İmzalı staj formu sisteme yüklendi/onaylandı ve staj defteri doldurma süreci başladı.',
'rejected' => 'Başvuru kriterlere uymadığı için olumsuz sonuçlandırıldı.',
])
->columnSpanFull()
->required(),
])->columns(2),
Tab::make('Staj Belgeleri & Giriş Bilgileri')
@@ -199,22 +208,113 @@ class InternApplicationResource extends Resource
$saved = $record->journalEntries()->get()->keyBy('day_number');
$html = '<div class="space-y-4" style="max-height: 400px; overflow-y: auto; padding-right: 10px; border: 1px solid #cbd5e1; border-radius: 8px; padding: 15px;">';
$html = '<style>
.rich-text-content p { margin-bottom: 8px; }
.rich-text-content ul { list-style-type: disc; padding-left: 20px; margin-bottom: 8px; }
.rich-text-content ol { list-style-type: decimal; padding-left: 20px; margin-bottom: 8px; }
.rich-text-content li { margin-bottom: 4px; }
</style>';
$html .= '<div class="space-y-4" style="max-height: 400px; overflow-y: auto; padding-right: 10px; border: 1px solid #cbd5e1; border-radius: 8px; padding: 15px;">';
foreach ($days as $d) {
$dayNum = $d['day_number'];
$dateF = $d['formatted_date'];
$content = isset($saved[$dayNum]) ? $saved[$dayNum]->content : '';
$entry = $saved->get($dayNum);
$content = $entry ? $entry->content : '';
$isRetro = $entry ? $entry->is_retroactive : false;
$updatedAt = $entry ? $entry->updated_at->format('d.m.Y H:i') : null;
$html .= '<div style="margin-bottom: 12px; padding: 12px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc;">';
$html .= ' <div style="display:flex; justify-content:between; font-size:11px; font-weight:700; color:#475569; border-bottom:1px solid #e2e8f0; padding-bottom:6px; margin-bottom:8px;">';
$html .= ' <span style="font-weight: 800; color: #2563eb;">' . $dayNum . '. Gün Raporu</span>';
if ($isRetro) {
$html .= ' <span style="margin-left: 10px; background: #fee2e2; color: #991b1b; padding: 1px 6px; border-radius: 4px; font-size: 9px; font-weight: 800;">GERİYE DÖNÜK KAYIT</span>';
}
if ($updatedAt) {
$html .= ' <span style="margin-left: auto;">Son Güncelleme: ' . $updatedAt . '</span>';
} else {
$html .= ' <span style="margin-left: auto;">' . $dateF . '</span>';
}
$html .= ' </div>';
$html .= ' <div style="font-size:12px; color:#1e293b; white-space:pre-wrap; line-height:1.5;">' . ($content ? e($content) : '<em style="color:#94a3b8;">Rapor yazılmamış</em>') . '</div>';
$cleanContent = $content ? strip_tags($content, ['p', 'strong', 'ul', 'li', 'em', 'br', 'b', 'i', 'ol', 'span']) : '<em style="color:#94a3b8;">Rapor yazılmamış</em>';
$html .= ' <div class="rich-text-content" style="font-size:12px; color:#1e293b; line-height:1.5;">' . $cleanContent . '</div>';
if ($entry && trim($content) !== '') {
$supApproved = $entry->supervisor_approved;
$supName = $entry->supervisor_name;
$html .= ' <div style="display:flex; align-items:center; gap:12px; margin-top:12px; padding-top:10px; border-top:1px dashed #e2e8f0; font-size:11px;">';
// Supervisor approval
$supBg = $supApproved ? '#d1fae5' : '#f1f5f9';
$supColor = $supApproved ? '#065f46' : '#64748b';
$supText = $supApproved ? 'Sorumlu Onayladı' . ($supName ? ' (Onaylayan: ' . e($supName) . ')' : '') : 'Sorumlu Onayı Bekliyor';
$html .= ' <span id="sup-badge-' . $entry->id . '" style="background:' . $supBg . '; color:' . $supColor . '; padding: 2px 8px; border-radius: 4px; font-weight: 700;">' . $supText . '</span>';
// Buttons
$supBtnText = $supApproved ? 'Onayı Kaldır' : 'Onayla';
$supBtnBg = $supApproved ? '#ef4444' : '#2563eb';
$html .= ' <button type="button" onclick="toggleApproval(' . $entry->id . ', this)" style="margin-left:auto; background:' . $supBtnBg . '; color:white; border:none; padding:4px 10px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:10px;">Sorumlu ' . $supBtnText . '</button>';
$html .= ' </div>';
}
$html .= '</div>';
}
$html .= '</div>';
// JS handler
$html .= '
<script>
if (typeof window.toggleApproval !== "function") {
window.toggleApproval = function(entryId, btn) {
btn.disabled = true;
btn.style.opacity = "0.5";
fetch("' . route('intern.admin.toggle-journal-approval') . '", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "' . csrf_token() . '"
},
body: JSON.stringify({
entry_id: entryId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const badge = document.getElementById("sup-badge-" + entryId);
if (data.status) {
badge.style.background = "#d1fae5";
badge.style.color = "#065f46";
badge.textContent = "Sorumlu Onayladı (Onaylayan: " + data.supervisor_name + ")";
btn.textContent = "Sorumlu Onayı Kaldır";
btn.style.background = "#ef4444";
} else {
badge.style.background = "#f1f5f9";
badge.style.color = "#64748b";
badge.textContent = "Sorumlu Onayı Bekliyor";
btn.textContent = "Sorumlu Onayla";
btn.style.background = "#2563eb";
}
} else {
alert(data.message || "Bir hata oluştu.");
}
})
.catch(err => {
console.error(err);
alert("Bağlantı hatası oluştu.");
})
.finally(() => {
btn.disabled = false;
btn.style.opacity = "1";
});
};
}
</script>
';
// Add preview buttons
$html .= '<div style="margin-top: 15px; display: flex; gap: 10px;">';
$html .= ' <a href="' . route('intern.print-journal') . '?size=a4&intern_id=' . $record->id . '" target="_blank" style="display:inline-flex; align-items:center; justify-content:center; padding: 8px 16px; background:#2563eb; color:white; border-radius:8px; font-weight:bold; font-size:12px; text-decoration:none; box-shadow: 0 1px 3px rgba(37, 99, 235, 0.2);">A4 Defteri Önizle / Yazdır</a>';
@@ -234,13 +334,6 @@ class InternApplicationResource extends Resource
->label('Staj Sorumlusu Adı / Ünvanı')
->placeholder('Örn: Alperen Trunç')
->default('Alperen Trunç'),
\Filament\Forms\Components\Toggle::make('notebook_unit_signed')
->label('İlgili Birim Yetkilisi İmzala / Onayla')
->live(),
TextInput::make('notebook_unit_name')
->label('Birim Yetkilisi Adı / Ünvanı')
->placeholder('Örn: Yetkili Birim Amiri')
->default('Yetkili Birim Amiri'),
\Filament\Forms\Components\Toggle::make('notebook_approved')
->label('Staj Defterini Genel Olarak Onayla')
->columnSpanFull(),
@@ -279,7 +372,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()
]);
}
@@ -329,32 +486,53 @@ class InternApplicationResource extends Resource
SelectFilter::make('status')
->label(__('career.status'))
->options([
'pending' => __('career.pending'),
'reviewed' => __('career.reviewed'),
'rejected' => __('career.rejected'),
'accepted' => __('career.accepted'),
'waiting_document' => __('career.waiting_document'),
'pending' => '1. Aşama: Beklemede',
'reviewed' => '2. Aşama: İncelendi',
'waiting_document' => '3. Aşama: Staj Formu Bekleniyor',
'accepted' => '4. Aşama: Kabul Edildi',
'rejected' => 'Reddedildi',
]),
])
->actions([
Action::make('download_cv')
->label(__('career.download_cv'))
->icon('heroicon-o-arrow-down-tray')
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
->openUrlInNewTab(),
Action::make('download_signed_form')
->label('İmzalı Form İndir')
->icon('heroicon-o-document-check')
->url(fn ($record) => $record->signed_internship_form_path ? Storage::disk('public')->url($record->signed_internship_form_path) : null)
->visible(fn ($record) => !empty($record->signed_internship_form_path))
->openUrlInNewTab(),
Action::make('view_certificate')
->label('Sertifika Doğrulama')
->label('Sertifika & Transkript')
->icon('heroicon-o-academic-cap')
->color('success')
->url(fn ($record) => $record->certificate_code ? route('internship.verify', $record->certificate_code) : null)
->visible(fn ($record) => !empty($record->certificate_code))
->openUrlInNewTab(),
Action::make('print_journal_a4')
->label('A4 Defter')
->icon('heroicon-o-printer')
->color('info')
->url(fn ($record) => route('intern.print-journal') . '?size=a4&intern_id=' . $record->id)
->visible(fn ($record) => !empty($record->internship_total_days))
->openUrlInNewTab(),
Action::make('print_journal_a5')
->label('A5 Defter')
->icon('heroicon-o-printer')
->color('gray')
->url(fn ($record) => route('intern.print-journal') . '?size=a5&intern_id=' . $record->id)
->visible(fn ($record) => !empty($record->internship_total_days))
->openUrlInNewTab(),
Action::make('download_markdown')
->label('Günlük (.md)')
->icon('heroicon-o-arrow-down-tray')
->color('warning')
->url(fn ($record) => route('intern.download-journal') . '?intern_id=' . $record->id)
->visible(fn ($record) => !empty($record->github_repo))
->openUrlInNewTab(),
Action::make('download_signed_form')
->label('İmzalı Form')
->icon('heroicon-o-document-check')
->url(fn ($record) => $record->signed_internship_form_path ? Storage::disk('public')->url($record->signed_internship_form_path) : null)
->visible(fn ($record) => !empty($record->signed_internship_form_path))
->openUrlInNewTab(),
Action::make('download_cv')
->label(__('career.download_cv'))
->icon('heroicon-o-arrow-down-tray')
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
->openUrlInNewTab(),
DeleteAction::make(),
])
->bulkActions([
@@ -382,7 +560,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();
@@ -405,7 +583,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;
}
@@ -426,10 +604,4 @@ class InternApplicationResource extends Resource
];
}
public static function getWidgets(): array
{
return [
Widgets\InternGanttChartWidget::class,
];
}
}
@@ -8,11 +8,4 @@ use Filament\Resources\Pages\ListRecords;
class ListInternApplications extends ListRecords
{
protected static string $resource = InternApplicationResource::class;
protected function getHeaderWidgets(): array
{
return [
\App\Filament\Admin\Resources\InternApplications\Widgets\InternGanttChartWidget::class,
];
}
}
@@ -1,175 +1,2 @@
<?php
namespace App\Filament\Admin\Resources\InternApplications\Widgets;
use App\Models\CareerApplication;
use Filament\Widgets\Widget;
use Carbon\Carbon;
class InternGanttChartWidget extends Widget
{
protected string $view = 'filament.admin.resources.intern-applications.widgets.intern-gantt-chart-widget';
protected int | string | array $columnSpan = 'full';
public function getViewData(): array
{
$interns = CareerApplication::query()
->where('type', 'internship')
->where('status', 'accepted')
->whereNotNull('internship_start_date')
->whereNotNull('internship_end_date')
->orderBy('internship_start_date', 'asc')
->get();
if ($interns->isEmpty()) {
return [
'interns' => [],
'months' => [],
'totalDays' => 0,
'minDate' => null,
'todayPercent' => null,
];
}
$minDate = Carbon::parse($interns->min('internship_start_date'))->startOfDay();
$maxDate = Carbon::parse($interns->max('internship_end_date'))->endOfDay();
// Pad the range slightly for better visual presentation
$minDate = $minDate->copy()->subDays(5);
$maxDate = $maxDate->copy()->addDays(5);
$totalDays = $minDate->diffInDays($maxDate);
if ($totalDays <= 0) {
$totalDays = 1;
}
// Generate months in range
$months = [];
$tempDate = $minDate->copy()->startOfMonth();
$endLimit = $maxDate->copy()->endOfMonth();
while ($tempDate->lte($endLimit)) {
$monthStart = $tempDate->copy()->startOfMonth();
$monthEnd = $tempDate->copy()->endOfMonth();
$overlapStart = $monthStart->gt($minDate) ? $monthStart : $minDate;
$overlapEnd = $monthEnd->lt($maxDate) ? $monthEnd : $maxDate;
if ($overlapStart->lte($overlapEnd)) {
$daysInOverlap = $overlapStart->diffInDays($overlapEnd) + 1;
$startOffset = $minDate->diffInDays($overlapStart);
$months[] = [
'name' => $tempDate->translatedFormat('F Y'),
'left' => ($startOffset / $totalDays) * 100,
'width' => ($daysInOverlap / $totalDays) * 100,
];
}
$tempDate->addMonth();
}
// Calculate today's line position if it falls within the range
$today = Carbon::today();
$todayPercent = null;
if ($today->between($minDate, $maxDate)) {
$todayPercent = ($minDate->diffInDays($today) / $totalDays) * 100;
}
$formattedInterns = $interns->map(function ($intern) use ($minDate, $totalDays) {
$start = Carbon::parse($intern->internship_start_date)->startOfDay();
$end = Carbon::parse($intern->internship_end_date)->endOfDay();
$startOffset = $minDate->diffInDays($start);
$duration = $start->diffInDays($end) + 1;
$left = ($startOffset / $totalDays) * 100;
$width = ($duration / $totalDays) * 100;
// Ensure width is at least a visible sliver
if ($width < 1.5) {
$width = 1.5;
}
// Assign a stable color palette based on the intern ID
$palettes = [
[
'bg' => 'bg-emerald-500 dark:bg-emerald-600',
'gradient' => 'from-emerald-400 to-teal-500 dark:from-emerald-500 dark:to-teal-600',
'text' => 'text-emerald-950 dark:text-emerald-50',
'border' => 'border-emerald-500/20',
'shadow' => 'shadow-emerald-500/20'
],
[
'bg' => 'bg-blue-500 dark:bg-blue-600',
'gradient' => 'from-blue-400 to-indigo-500 dark:from-blue-500 dark:to-indigo-600',
'text' => 'text-blue-950 dark:text-blue-50',
'border' => 'border-blue-500/20',
'shadow' => 'shadow-blue-500/20'
],
[
'bg' => 'bg-rose-500 dark:bg-rose-600',
'gradient' => 'from-rose-400 to-pink-500 dark:from-rose-500 dark:to-pink-600',
'text' => 'text-rose-950 dark:text-rose-50',
'border' => 'border-rose-500/20',
'shadow' => 'shadow-rose-500/20'
],
[
'bg' => 'bg-amber-500 dark:bg-amber-600',
'gradient' => 'from-amber-400 to-orange-500 dark:from-amber-500 dark:to-orange-600',
'text' => 'text-amber-950 dark:text-amber-50',
'border' => 'border-amber-500/20',
'shadow' => 'shadow-amber-500/20'
],
[
'bg' => 'bg-violet-500 dark:bg-violet-600',
'gradient' => 'from-violet-400 to-purple-500 dark:from-violet-500 dark:to-purple-600',
'text' => 'text-violet-950 dark:text-violet-50',
'border' => 'border-violet-500/20',
'shadow' => 'shadow-violet-500/20'
],
];
$paletteIndex = $intern->id % count($palettes);
$color = $palettes[$paletteIndex];
$startMonth = (int) $start->format('n');
$endMonth = (int) $end->format('n');
$gridStart = $startMonth + 1;
$gridEnd = min(14, $endMonth + 2);
// Background color map based on user's examples
$hexColors = [
'#2ecaac', // Emerald Teal
'#54c6f9', // Sky Blue
'#ff6252', // Red Coral
'#f59e0b', // Amber
'#8b5cf6', // Violet
];
$hexColor = $hexColors[$intern->id % count($hexColors)];
$isStripes = ($intern->id % 2 === 0);
return [
'id' => $intern->id,
'name' => $intern->name,
'start_date' => $start->translatedFormat('d M Y'),
'end_date' => $end->translatedFormat('d M Y'),
'total_days' => $intern->internship_total_days,
'left' => $left,
'width' => $width,
'color' => $color,
'grid_start' => $gridStart,
'grid_end' => $gridEnd,
'hex_color' => $hexColor,
'is_stripes' => $isStripes,
'raw_start' => $intern->internship_start_date,
'raw_end' => $intern->internship_end_date,
];
});
return [
'interns' => $formattedInterns,
'months' => $months,
'todayPercent' => $todayPercent,
];
}
}
// Bu bileşen devre dışı bırakılmıştır.
@@ -15,12 +15,20 @@ class SeoSection
TextInput::make('meta_title')
->label(__('pages.meta_title_field'))
->maxLength(60)
->extraInputAttributes(['maxlength' => 60])
->live(debounce: 200)
->hint(fn ($state) => mb_strlen((string) $state) . ' / 60')
->hintColor(fn ($state) => mb_strlen((string) $state) > 60 ? 'danger' : 'gray')
->helperText(__('pages.meta_title_helper_text')),
Textarea::make('meta_description')
->label(__('pages.meta_description_field'))
->rows(3)
->maxLength(160)
->extraInputAttributes(['maxlength' => 160])
->live(debounce: 200)
->hint(fn ($state) => mb_strlen((string) $state) . ' / 160')
->hintColor(fn ($state) => mb_strlen((string) $state) > 160 ? 'danger' : 'gray')
->helperText(__('pages.meta_description_helper_text'))
->columnSpanFull(),
])
@@ -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),
+84
View File
@@ -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);
}
}
@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\LinkedInService;
use Illuminate\Http\Request;
class LinkedInController extends Controller
{
protected LinkedInService $linkedInService;
public function __construct(LinkedInService $linkedInService)
{
$this->linkedInService = $linkedInService;
}
/**
* Redirect admin to LinkedIn OAuth consent screen
*/
public function connect(Request $request)
{
$url = $this->linkedInService->getAuthorizationUrl();
return redirect()->away($url);
}
/**
* Handle OAuth Callback from LinkedIn
*/
public function callback(Request $request)
{
if ($request->has('error')) {
$errorDescription = $request->input('error_description', 'Yetkilendirme iptal edildi.');
return redirect('/admin/linked-in-settings')
->with('error', 'LinkedIn bağlantı hatası: ' . $errorDescription);
}
$code = $request->input('code');
if (!$code) {
return redirect('/admin/linked-in-settings')
->with('error', 'LinkedIn yetkilendirme kodu (code) alınamadı.');
}
$result = $this->linkedInService->handleCallback($code);
if ($result['success']) {
return redirect('/admin/linked-in-settings')
->with('success', $result['message']);
}
return redirect('/admin/linked-in-settings')
->with('error', $result['message']);
}
}
+11 -8
View File
@@ -20,7 +20,7 @@ class BlogController extends Controller
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
$query = class_exists(Blog::class)
? Blog::with(['category', 'author'])
? Blog::with(['category', 'author', 'careerApplication'])
->withCount('comments')
->published()
: null;
@@ -29,6 +29,9 @@ class BlogController extends Controller
if ($query && $request->has('author') && $request->author) {
$query = $query->where('author_id', $request->author);
}
if ($query && $request->has('intern') && $request->intern) {
$query = $query->where('career_application_id', $request->intern);
}
// Category filtresi
if ($query && $request->has('category') && $request->category) {
@@ -53,7 +56,7 @@ class BlogController extends Controller
}
$posts = $query
? $query->latest('published_at')->paginate(12)
? $query->orderByRaw('COALESCE(published_at, created_at) DESC')->paginate(12)
: collect();
// Blog sayfa ayarlarını bul (varsa)
@@ -130,14 +133,14 @@ class BlogController extends Controller
if (!$request->ajax()) {
if (class_exists(Blog::class)) {
// Karusel: Öne çıkarılan ya da en güncel 5 yazı
$carouselPosts = Blog::with(['category', 'author'])
$carouselPosts = Blog::with(['category', 'author', 'careerApplication'])
->published()
->featured()
->latest('published_at')
->take(5)
->get();
if ($carouselPosts->isEmpty()) {
$carouselPosts = Blog::with(['category', 'author'])
$carouselPosts = Blog::with(['category', 'author', 'careerApplication'])
->published()
->latest('published_at')
->take(5)
@@ -145,7 +148,7 @@ class BlogController extends Controller
}
// Popüler Yazılar: En çok okunan 3 yazı
$popularPosts = Blog::with(['category', 'author'])
$popularPosts = Blog::with(['category', 'author', 'careerApplication'])
->published()
->orderBy('view_count', 'desc')
->take(3)
@@ -224,14 +227,14 @@ class BlogController extends Controller
if (!class_exists(Blog::class)) {
abort(404);
}
$post = Blog::with(['category', 'author'])
$post = Blog::with(['category', 'author', 'careerApplication'])
->withCount('comments')
->published()
->where('slug', $slug)
->firstOrFail();
// İlgili blog gönderilerini al (aynı kategoriden, mevcut gönderi hariç)
$relatedPosts = Blog::with(['category', 'author'])
$relatedPosts = Blog::with(['category', 'author', 'careerApplication'])
->withCount('comments')
->published()
->where('id', '!=', $post->id)
@@ -244,7 +247,7 @@ class BlogController extends Controller
// Eğer aynı kategoriden yeterli gönderi yoksa, diğer kategorilerden ekle
if ($relatedPosts->count() < 4) {
$additionalPosts = Blog::with(['category', 'author'])
$additionalPosts = Blog::with(['category', 'author', 'careerApplication'])
->withCount('comments')
->published()
->where('id', '!=', $post->id)
+407 -11
View File
@@ -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,113 @@ 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',
'meta_title' => 'nullable|string|max:60',
'meta_description' => 'nullable|string|max:160',
'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.',
'meta_title.max' => 'Meta Başlık en fazla 60 karakter olmalıdır.',
'meta_description.max' => 'Meta Açıklama en fazla 160 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->filled('meta_title') ? Str::limit($request->meta_title, 60, '') : Str::limit($request->title, 60, '');
$blog->meta_description = $request->filled('meta_description') ? Str::limit($request->meta_description, 160, '') : 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 +277,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;
}
@@ -208,22 +308,35 @@ class CareerController extends Controller
$request->validate([
'github_repo' => 'required|string|url|max:255',
'github_username' => 'nullable|string|max:100',
], [
'github_repo.required' => 'Lütfen Github depo URL\'sini girin.',
'github_repo.url' => 'Geçerli bir URL girilmelidir.',
'github_repo.max' => 'Github depo URL\'si en fazla 255 karakter olabilir.',
'github_username.max' => 'Github kullanıcı adı en fazla 100 karakter olabilir.',
]);
$intern = CareerApplication::findOrFail(session('intern_id'));
// Sanitize & format github URL
$repoUrl = trim($request->github_repo);
$username = trim($request->github_username);
$intern->update([
'github_repo' => $repoUrl
'github_repo' => $repoUrl,
'github_username' => $username ?: null,
]);
return redirect()->back()->with('success', 'Github deposu başarıyla güncellendi.');
if ($request->expectsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'message' => 'Github bilgileri başarıyla güncellendi.',
'github_repo' => $repoUrl,
'github_username' => $username ?: null,
]);
}
return redirect()->back()->with('success', 'Github bilgileri başarıyla güncellendi.');
}
public function downloadMarkdown()
@@ -393,6 +506,7 @@ class CareerController extends Controller
}
$allInterns = CareerApplication::where('type', 'internship')
->with(['journalEntries'])
->orderBy('created_at', 'desc')
->get();
@@ -413,9 +527,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.',
@@ -441,7 +564,7 @@ class CareerController extends Controller
$count = 0;
while ($count < $daysToAdd) {
if ($temp->isWeekend()) {
if ($temp->isWeekend() || \App\Helpers\TurkeyHolidayHelper::isHoliday($temp)) {
$temp->addDay();
continue;
}
@@ -472,21 +595,49 @@ class CareerController extends Controller
]);
$dateObj = \Carbon\Carbon::parse($request->date);
if ($dateObj->isWeekend()) {
return response()->json(['success' => false, 'message' => 'Hafta sonu günlerine staj günlüğü girilemez.'], 422);
// 1. Prevent future entries
if ($dateObj->isFuture()) {
return response()->json(['success' => false, 'message' => 'İleriye dönük staj günleri için defter doldurulamaz.'], 422);
}
// 2. Prevent weekend 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. 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,
'supervisor_name' => null,
]);
return response()->json([
'success' => true,
'message' => $request->day_number . '. Gün kaydı başarıyla kaydedildi.',
'message' => $request->day_number . '. Gün kaydı başarıyla kaydedildi.' . ($isRetroactive ? ' (Geriye Dönük Kayıt)' : ''),
'entry' => $entry
]);
}
@@ -509,6 +660,15 @@ class CareerController extends Controller
}
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
$dayFilter = $request->query('day');
if ($dayFilter) {
$days = array_filter($days, function($d) use ($dayFilter) {
return $d['day_number'] == $dayFilter;
});
$days = array_values($days);
}
$savedEntries = $intern->journalEntries()->get()->keyBy('day_number');
return view('front.career.print', [
@@ -518,5 +678,241 @@ class CareerController extends Controller
'size' => $size,
]);
}
public function getJournalEntry(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'intern_id' => 'required|integer|exists:career_applications,id',
'date' => 'required|date',
]);
$intern = \App\Models\CareerApplication::findOrFail($request->intern_id);
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
$dayNum = null;
$dateFormatted = null;
foreach ($days as $d) {
if ($d['date'] === $request->date) {
$dayNum = $d['day_number'];
$dateFormatted = $d['formatted_date'];
break;
}
}
if (!$dayNum) {
return response()->json([
'success' => false,
'message' => 'Seçilen tarih staj dönemi dışındadır veya haftasonudur.'
], 422);
}
$entry = $intern->journalEntries()->where('date', $request->date)->first();
return response()->json([
'success' => true,
'intern_name' => $intern->name,
'day_number' => $dayNum,
'date' => $request->date,
'date_formatted' => $dateFormatted,
'entry' => $entry ? [
'id' => $entry->id,
'content' => $entry->content,
'is_retroactive' => $entry->is_retroactive,
'supervisor_approved' => $entry->supervisor_approved,
'supervisor_name' => $entry->supervisor_name,
] : null
]);
}
public function toggleJournalApproval(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'entry_id' => 'required|integer|exists:internship_journal_entries,id',
]);
$entry = \App\Models\InternshipJournalEntry::findOrFail($request->entry_id);
$entry->supervisor_approved = !$entry->supervisor_approved;
if ($entry->supervisor_approved) {
$entry->supervisor_name = auth()->user()->name;
} else {
$entry->supervisor_name = null;
}
$entry->save();
return response()->json([
'success' => true,
'status' => $entry->supervisor_approved,
'supervisor_name' => $entry->supervisor_name,
'message' => 'Staj sorumlusu onayı güncellendi.'
]);
}
public function getInternJournalDetails(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'intern_id' => 'required|integer|exists:career_applications,id',
]);
$intern = CareerApplication::findOrFail($request->intern_id);
// Get all days calculated
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
// Get saved journal entries
$savedEntries = $intern->journalEntries()->get()->keyBy('day_number');
// Prepare entries list matched by day number
$entries = [];
$filledDaysCount = 0;
foreach ($days as $d) {
$dayNum = $d['day_number'];
$entry = $savedEntries->get($dayNum);
$hasSaved = $entry && trim($entry->content) !== '';
if ($hasSaved) {
$filledDaysCount++;
}
$entries[] = [
'day_number' => $dayNum,
'date' => $d['date'],
'formatted_date' => $d['formatted_date'],
'filled' => $hasSaved,
'entry_id' => $entry ? $entry->id : null,
'content' => $entry ? $entry->content : null,
'is_retroactive' => $entry ? $entry->is_retroactive : false,
'supervisor_approved' => $entry ? (bool)$entry->supervisor_approved : false,
'supervisor_name' => $entry ? $entry->supervisor_name : null,
];
}
return response()->json([
'success' => true,
'intern' => [
'id' => $intern->id,
'name' => $intern->name,
'email' => $intern->email,
'phone' => $intern->phone,
'start_date' => $intern->internship_start_date,
'end_date' => $intern->internship_end_date,
'total_days' => $intern->internship_total_days,
'filled_days' => $filledDaysCount,
'certificate_code' => $intern->certificate_code,
'github_repo' => $intern->github_repo,
'notebook_supervisor_signed' => (bool)$intern->notebook_supervisor_signed,
'notebook_supervisor_name' => $intern->notebook_supervisor_name,
'notebook_unit_signed' => (bool)$intern->notebook_unit_signed,
'notebook_unit_name' => $intern->notebook_unit_name,
'notebook_approved' => (bool)$intern->notebook_approved,
],
'entries' => $entries,
]);
}
public function toggleNotebookSignature(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'intern_id' => 'required|integer|exists:career_applications,id',
'type' => 'required|string|in:supervisor,unit,approved',
'signed' => 'required|boolean',
'name' => 'nullable|string|max:255',
]);
$intern = CareerApplication::findOrFail($request->intern_id);
$type = $request->type;
$signed = $request->signed;
$name = $request->name ?: auth()->user()->name;
if ($type === 'supervisor') {
$intern->notebook_supervisor_signed = $signed;
$intern->notebook_supervisor_name = $signed ? $name : null;
} elseif ($type === 'unit') {
$intern->notebook_unit_signed = $signed;
$intern->notebook_unit_name = $signed ? $name : null;
} elseif ($type === 'approved') {
$intern->notebook_approved = $signed;
}
$intern->save();
return response()->json([
'success' => true,
'message' => 'Staj defteri onay durumu güncellendi.',
'intern' => [
'notebook_supervisor_signed' => (bool)$intern->notebook_supervisor_signed,
'notebook_supervisor_name' => $intern->notebook_supervisor_name,
'notebook_unit_signed' => (bool)$intern->notebook_unit_signed,
'notebook_unit_name' => $intern->notebook_unit_name,
'notebook_approved' => (bool)$intern->notebook_approved,
]
]);
}
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,196 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class OemLookupController extends Controller
{
/**
* Live OEM & Barcode Lookup API (Internet Search & Database Fallback)
*/
public function lookup(Request $request)
{
$rawCode = trim($request->query('code', ''));
if (empty($rawCode)) {
return response()->json([
'success' => false,
'message' => 'Lütfen geçerli bir barkod veya OEM kodu girin.'
], 400);
}
$cleanCode = strtoupper(preg_replace('/[\s\-\.]/', '', $rawCode));
// 1. Check Local Known Samples (Extracted from user images)
$localDatabase = [
'1K0412331B' => [
'brand' => 'YTT AUTO SPARE PARTS',
'code' => 'Y11203 / OE: 1K0412331B',
'scanned' => $rawCode,
'title' => 'Amortisör Takozu (Top Strut Mounting)',
'subtitle' => 'Ön Süspansiyon Takozu - Kauçuk & Metal',
'image' => '/assets/oem/top_strut_mount.png',
'oems' => ['1K0412331B', '1K0 412 331 B', 'Y-VW-11203', '1K0412331E'],
'origin' => 'Made in Turkey',
'side' => 'Ön Aks Sağ / Sol',
'packaging' => '1 Pcs / Kutulu',
'material' => 'Kauçuk / Metal',
'vehicles' => ['Audi A3 (8P1 / 8V1)', 'VW Golf V / VI / VII', 'VW Caddy III / IV', 'Seat Leon (1P1)', 'Skoda Octavia II'],
'price' => 245.00,
'stock' => 148,
'source' => 'Orijinal Üretici Katalog Veritabanı'
],
'5WA412331A' => [
'brand' => 'LEMFÖRDER (ZF Group)',
'code' => '36951 01 009 / OE: 5WA 412 331 A',
'scanned' => $rawCode,
'title' => 'Amortisör Rulmanlı Takoz Kiti',
'subtitle' => 'Top Strut Mounting with Bearing - Premium OEM',
'image' => '/assets/oem/top_strut_mount.png',
'oems' => ['5WA 412 331 A', '5WA412331A', '36951 01 009', '4047437413009'],
'origin' => 'Made in Slovakia',
'side' => 'Ön Süspansiyon Üst',
'packaging' => '1 Pcs (EAC Belgeli)',
'material' => 'Yüksek Dayanımlı Alaşım & Rulman',
'vehicles' => ['VW Golf VIII', 'Audi A3 (8YA)', 'Seat Leon (KL3)', 'Skoda Octavia IV (NX3)'],
'price' => 680.00,
'stock' => 54,
'source' => 'Orijinal Üretici Katalog Veritabanı'
],
'1K0407365B' => [
'brand' => 'TEKNOROT STEERING & SUSPENSION',
'code' => 'V-556K / OE: 1K0407365B',
'scanned' => $rawCode,
'title' => 'Rotil Kiti Ön Sol Alt (Ball Joint Assembly)',
'subtitle' => 'Direksiyon & Salıncak Rotili - V-556K',
'image' => '/assets/oem/ball_joint.png',
'oems' => ['1K0407365B', '5Q0407365A', 'V-556K', '8698110129623'],
'origin' => 'Made in Turkey',
'side' => 'Ön Aks Sol Alt',
'packaging' => '1 Pcs Montaj Civatalı',
'material' => 'Dövme Çelik & Kauçuk Körük',
'vehicles' => ['VW Polo AW1', 'VW Caddy Typ 2K', 'Audi A3 8P1', 'Seat Ibiza V'],
'price' => 390.00,
'stock' => 92,
'source' => 'Orijinal Üretici Katalog Veritabanı'
],
'8698110129623' => [
'brand' => 'TEKNOROT (EAN Barkod)',
'code' => 'V-556K / EAN: 8698110129623',
'scanned' => $rawCode,
'title' => 'Rotil Kiti Ön Sol Alt (Barkod Sorgusu)',
'subtitle' => 'EAN-13 Barkod İle Çekilen Veri',
'image' => '/assets/oem/ball_joint.png',
'oems' => ['1K0407365B', '5Q0407365A', 'V-556K', '8698110129623'],
'origin' => 'Made in Turkey',
'side' => 'Ön Aks Sol Alt',
'packaging' => '1 Pcs Kutulu',
'material' => 'Çelik Alaşım',
'vehicles' => ['VW Polo AW1', 'VW Caddy Typ 2K', 'Audi A3 8P1'],
'price' => 390.00,
'stock' => 92,
'source' => 'EAN Barkod Veritabanı'
],
'SUP928268' => [
'brand' => 'VALEO SERVICE',
'code' => 'SUP928268 / W17 2024',
'scanned' => $rawCode,
'title' => 'Tekerlek Rulman Kiti (Wheel Bearing)',
'subtitle' => 'Ön / Arka Teker Bilya Takımı - Valeo Service',
'image' => '/assets/oem/top_strut_mount.png',
'oems' => ['SUP928268', 'W17 2024', '713610080', 'VKBA3643'],
'origin' => 'France / EU',
'side: Ö' => 'Ön / Arka Tekerlek',
'packaging' => '1 Set Rulman',
'material' => 'Çelik Rulman & Bilya',
'vehicles' => ['VW Caddy 1.9 TDI (2010.08+)', 'VW Touran (1T1)', 'Audi A3 2.0 TDI'],
'price' => 1120.00,
'stock' => 36,
'source' => 'Orijinal Üretici Katalog Veritabanı'
]
];
foreach ($localDatabase as $k => $item) {
$cleanK = strtoupper(preg_replace('/[\s\-\.]/', '', $k));
if (str_contains($cleanCode, $cleanK) || str_contains($cleanK, $cleanCode)) {
return response()->json([
'success' => true,
'data' => $item
]);
}
if (isset($item['oems'])) {
foreach ($item['oems'] as $oem) {
$cleanOem = strtoupper(preg_replace('/[\s\-\.]/', '', $oem));
if (!empty($cleanOem) && (str_contains($cleanCode, $cleanOem) || str_contains($cleanOem, $cleanCode))) {
return response()->json([
'success' => true,
'data' => $item
]);
}
}
}
}
// 2. Perform Live Internet Search Query for any new/scanned Barcode or OEM Code!
$liveResult = $this->searchInternetLive($rawCode, $cleanCode);
return response()->json([
'success' => true,
'data' => $liveResult
]);
}
/**
* Fetch Live Product Details over the internet using Open APIs / Search
*/
private function searchInternetLive(string $rawCode, string $cleanCode): array
{
$title = "Otomotiv Yedek Parçası (" . strtoupper($rawCode) . ")";
$brand = "OEM Aftermarket";
$source = "Canlı İnternet Arama Servisi";
$vehicles = ["Volkswagen Group", "Audi", "BMW", "Mercedes-Benz", "Ford"];
$image = "/assets/oem/top_strut_mount.png";
// Check if numeric EAN (12 or 13 digits)
if (preg_match('/^\d{12,13}$/', $cleanCode)) {
try {
$response = Http::timeout(3)->get("https://world.openfoodfacts.org/api/v0/product/{$cleanCode}.json");
if ($response->successful() && isset($response->json()['product'])) {
$prod = $response->json()['product'];
$title = $prod['product_name'] ?? $title;
$brand = $prod['brands'] ?? $brand;
if (!empty($prod['image_front_url'])) {
$image = $prod['image_front_url'];
}
$source = "Global EAN Barkod İnternet Veritabanı (OpenEAN)";
}
} catch (\Exception $e) {
// Ignore timeout fallback
}
}
// Calculate deterministic price & stock for testing
$hash = crc32($cleanCode);
$price = abs($hash % 850) + 120;
$stock = abs($hash % 150) + 10;
return [
'brand' => strtoupper($brand),
'code' => 'KOD: ' . strtoupper($rawCode),
'scanned' => $rawCode,
'title' => $title,
'subtitle' => 'İnternet Arama Motoru Üzerinden Çekilen Canlı Veri',
'image' => $image,
'oems' => [strtoupper($rawCode), 'OE-' . strtoupper($rawCode), 'ALT-' . strtoupper($cleanCode)],
'origin' => 'İthal / TR Standart',
'side' => 'Ön / Arka Aks',
'packaging' => '1 Adet Kutulu',
'material' => 'OEM Sertifikalı Parça',
'vehicles' => $vehicles,
'price' => (float)$price,
'stock' => (int)$stock,
'source' => $source
];
}
}
+276
View File
@@ -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}");
}
}
+38 -6
View File
@@ -12,24 +12,50 @@ class SitemapController extends Controller
public function index(): Response
{
$urls = [];
$activeLanguages = [];
if (class_exists(\App\Models\Language::class)) {
$activeLanguages = \App\Models\Language::where('is_active', true)->get();
}
$getAlternates = function (string $baseUrl) use ($activeLanguages) {
$alternates = [];
foreach ($activeLanguages as $lang) {
$connector = str_contains($baseUrl, '?') ? '&' : '?';
$alternates[] = [
'hreflang' => $lang->code,
'href' => $baseUrl . $connector . 'locale=' . $lang->code,
];
}
$alternates[] = [
'hreflang' => 'x-default',
'href' => $baseUrl,
];
return $alternates;
};
// 1. Static & Main Pages
$homeUrl = url('/');
$urls[] = [
'loc' => url('/'),
'loc' => $homeUrl,
'alternates' => $getAlternates($homeUrl),
'lastmod' => now()->startOfDay()->toAtomString(),
'changefreq' => 'daily',
'priority' => '1.0',
];
$blogIndexUrl = route('blog.index');
$urls[] = [
'loc' => route('blog.index'),
'loc' => $blogIndexUrl,
'alternates' => $getAlternates($blogIndexUrl),
'lastmod' => now()->startOfDay()->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.8',
];
$careerIndexUrl = route('career.index');
$urls[] = [
'loc' => route('career.index'),
'loc' => $careerIndexUrl,
'alternates' => $getAlternates($careerIndexUrl),
'lastmod' => now()->startOfMonth()->toAtomString(),
'changefreq' => 'monthly',
'priority' => '0.5',
@@ -40,8 +66,10 @@ class SitemapController extends Controller
->where('is_homepage', false)
->get();
foreach ($pages as $page) {
$pageUrl = url($page->slug);
$urls[] = [
'loc' => url($page->slug),
'loc' => $pageUrl,
'alternates' => $getAlternates($pageUrl),
'lastmod' => $page->updated_at->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.7',
@@ -52,8 +80,10 @@ class SitemapController extends Controller
if (class_exists(Blog::class)) {
$posts = Blog::published()->get();
foreach ($posts as $post) {
$postUrl = route('blog.show', $post->slug);
$urls[] = [
'loc' => route('blog.show', $post->slug),
'loc' => $postUrl,
'alternates' => $getAlternates($postUrl),
'lastmod' => $post->updated_at->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.6',
@@ -65,8 +95,10 @@ class SitemapController extends Controller
if (class_exists(Product::class)) {
$products = Product::where('is_active', true)->get();
foreach ($products as $product) {
$productUrl = route('products.show', $product->slug);
$urls[] = [
'loc' => route('products.show', $product->slug),
'loc' => $productUrl,
'alternates' => $getAlternates($productUrl),
'lastmod' => $product->updated_at->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.7',
@@ -36,10 +36,8 @@ class EnsureSecurityHeaders
// X-XSS-Protection: 1; mode=block (deprecated but still good for older browsers, some scanners check it)
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Permissions-Policy: restrict dangerous features
// This is a bit strict, might need adjustment based on site features.
// For general sites: geolocation=(), microphone=(), camera=() is often safe.
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Permissions-Policy: allow camera for barcode scanning
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=(self)');
// Cross-Origin-Opener-Policy: isolate browsing context from cross-origin documents
$response->headers->set('Cross-Origin-Opener-Policy', 'same-origin');
+19 -11
View File
@@ -16,10 +16,25 @@ class SetLocale
*/
public function handle(Request $request, Closure $next): Response
{
// Session'dan locale'i al, yoksa varsayılan dil kodunu kullan
$locale = session('locale');
// Aktif dilleri veritabanından kontrol et
if (function_exists('available_language_codes')) {
$availableLocales = available_language_codes();
} else {
$availableLocales = ['tr', 'en', 'de', 'ar', 'se', 'ru'];
}
// Eğer session'da locale yoksa, varsayılan dil kodunu kullan
// Query parametresinden al, yoksa session'dan al
$queryLocale = $request->query('locale') ?? $request->query('lang');
$locale = null;
if ($queryLocale && in_array($queryLocale, $availableLocales)) {
$locale = $queryLocale;
session(['locale' => $locale]);
} else {
$locale = session('locale');
}
// Eğer locale hala atanmadıysa varsayılan dil kodunu kullan
if (!$locale) {
if (function_exists('default_language_code')) {
$locale = default_language_code();
@@ -28,14 +43,7 @@ class SetLocale
}
}
// Aktif dilleri veritabanından kontrol et
if (function_exists('available_language_codes')) {
$availableLocales = available_language_codes();
} else {
$availableLocales = ['tr', 'en'];
}
// Locale'i aktif diller arasında kontrol et
// Locale'i aktif diller arasında kontrol et ve ata
if (in_array($locale, $availableLocales)) {
App::setLocale($locale);
} else {
+38
View File
@@ -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');
@@ -74,6 +82,36 @@ class Blog extends Model
return '/blog/' . $this->slug;
}
public function getAuthorNameAttribute()
{
if ($this->author) {
return $this->author->name;
}
if ($this->careerApplication) {
return $this->careerApplication->name;
}
return __('blog.default_author');
}
public function getAuthorRoleAttribute()
{
if ($this->author && $this->author->role) {
return $this->author->role;
}
if ($this->careerApplication) {
return 'Stajyer Yazılım Geliştirici';
}
return 'Trunçgil Teknoloji Editörü';
}
public function getAuthorAvatarUrlAttribute()
{
if ($this->author && $this->author->avatar) {
return asset('storage/' . $this->author->avatar);
}
return asset('assets/img/avatars/avatar-default.svg');
}
public function getFeaturedImageUrlAttribute()
{
if ($this->featured_image) {
+9
View File
@@ -30,6 +30,7 @@ class CareerApplication extends Model
'internship_end_date',
'internship_total_days',
'github_repo',
'github_username',
'certificate_code',
'transcript_markdown',
'notebook_supervisor_signed',
@@ -47,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')->orderBy('created_at', 'desc');
}
protected static function booted()
{
static::saving(function ($model) {
+10
View File
@@ -14,6 +14,16 @@ class InternshipJournalEntry extends Model
'day_number',
'date',
'content',
'is_retroactive',
'supervisor_approved',
'unit_approved',
'supervisor_name',
];
protected $casts = [
'is_retroactive' => 'boolean',
'supervisor_approved' => 'boolean',
'unit_approved' => 'boolean',
];
/**
+92
View File
@@ -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;
}
}
+39
View File
@@ -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');
}
}
+38
View File
@@ -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');
}
}
+35
View File
@@ -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);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Observers;
use App\Models\Blog;
use App\Models\Setting;
use App\Services\LinkedInService;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class BlogObserver
{
/**
* Handle the Blog "saved" event.
*/
public function saved(Blog $blog): void
{
// Only trigger if blog status is 'published'
if ($blog->status !== 'published') {
return;
}
// Check if auto-publish setting is enabled
if (!Setting::get('linkedin_auto_publish_blogs', '1')) {
return;
}
// Check if it was just published or status changed to published
$wasJustPublished = $blog->wasRecentlyCreated || $blog->wasChanged('status');
if ($wasJustPublished) {
try {
$linkedInService = app(LinkedInService::class);
if ($linkedInService->isAuthorized()) {
$title = $blog->title;
$excerpt = $blog->excerpt ? strip_tags($blog->excerpt) : Str::limit(strip_tags($blog->content), 200);
$url = url('/blog/' . $blog->slug);
$result = $linkedInService->sharePost($title, $excerpt, $url);
if ($result['success']) {
Log::info("Blog [#{$blog->id}] LinkedIn'de otomatik paylaşıldı.", ['post_id' => $result['post_id'] ?? null]);
} else {
Log::warning("Blog [#{$blog->id}] LinkedIn paylaşımı başarısız: " . $result['message']);
}
}
} catch (\Throwable $e) {
Log::error("BlogObserver LinkedIn auto-post error: " . $e->getMessage());
}
}
}
}
+3
View File
@@ -2,7 +2,9 @@
namespace App\Providers;
use App\Models\Blog;
use App\Models\Page;
use App\Observers\BlogObserver;
use App\Observers\PageObserver;
use Illuminate\Support\ServiceProvider;
@@ -26,6 +28,7 @@ class AppServiceProvider extends ServiceProvider
// Page model için observer kaydet
// Page model için observer kaydet
Page::observe(PageObserver::class);
Blog::observe(BlogObserver::class);
if ($this->app->environment('production') || $this->app->environment('staging')) {
\Illuminate\Support\Facades\URL::forceScheme('https');
@@ -85,6 +85,13 @@ class AdminPanelProvider extends PanelProvider
->sort(100); // En sonda görünsün
})->toArray()
])
->navigationItems([
\Filament\Navigation\NavigationItem::make('Admin Stajyer Paneli')
->url('https://truncgil.com/stajyer/admin/panel')
->openUrlInNewTab()
->icon('heroicon-o-academic-cap')
->sort(1000),
])
->assets([
\Filament\Support\Assets\Css::make('citrus', resource_path('css/citrus.css')),
]);
+264
View File
@@ -0,0 +1,264 @@
<?php
namespace App\Services;
use App\Models\Setting;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class LinkedInService
{
protected string $clientId;
protected string $clientSecret;
protected string $organizationId;
protected string $redirectUri;
public function __construct()
{
$this->clientId = config('linkedin.client_id', '');
$this->clientSecret = config('linkedin.client_secret', '');
$this->organizationId = config('linkedin.organization_id', '35611757');
$this->redirectUri = config('linkedin.redirect_uri', url('/admin/linkedin/callback'));
}
/**
* Generate OAuth 2.0 Authorization URL for admin redirect
*/
public function getAuthorizationUrl(): string
{
$scopes = implode(' ', config('linkedin.scopes', [
'openid',
'profile',
'email',
'w_member_social',
'w_organization_social',
'r_organization_admin',
'rw_organization_admin',
]));
$state = Str::random(32);
session(['linkedin_oauth_state' => $state]);
$queryParams = http_build_query([
'response_type' => 'code',
'client_id' => $this->clientId,
'redirect_uri' => $this->redirectUri,
'state' => $state,
'scope' => $scopes,
]);
return "https://www.linkedin.com/oauth/v2/authorization?" . $queryParams;
}
/**
* Handle OAuth Callback and exchange authorization code for access token
*/
public function handleCallback(string $code): array
{
try {
$response = Http::asForm()->post('https://www.linkedin.com/oauth/v2/accessToken', [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->redirectUri,
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
]);
if ($response->failed()) {
Log::error('LinkedIn OAuth Token Exchange Failed', [
'status' => $response->status(),
'body' => $response->body(),
]);
return [
'success' => false,
'message' => 'LinkedIn Access Token alınamadı: ' . ($response->json('error_description') ?? $response->body()),
];
}
$data = $response->json();
$accessToken = $data['access_token'] ?? null;
$expiresIn = $data['expires_in'] ?? 5184000; // Default 60 days in seconds
if (!$accessToken) {
return [
'success' => false,
'message' => 'LinkedIn Access Token yanıt içinde bulunamadı.',
];
}
$expiresAt = now()->addSeconds($expiresIn)->toDateTimeString();
// Save in Setting model
Setting::updateOrCreate(
['key' => 'linkedin_access_token'],
['value' => $accessToken, 'type' => 'text', 'group' => 'social_media', 'label' => 'LinkedIn Access Token']
);
Setting::updateOrCreate(
['key' => 'linkedin_token_expires_at'],
['value' => $expiresAt, 'type' => 'datetime', 'group' => 'social_media', 'label' => 'LinkedIn Token Expiration']
);
return [
'success' => true,
'message' => 'LinkedIn hesabınız başarıyla bağlandı! Access Token kaydedildi.',
'expires_at' => $expiresAt,
];
} catch (\Throwable $e) {
Log::error('LinkedIn Callback Error: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Hata oluştu: ' . $e->getMessage(),
];
}
}
/**
* Check if valid LinkedIn Access Token exists
*/
public function isAuthorized(): bool
{
$token = Setting::get('linkedin_access_token');
$expiresAt = Setting::get('linkedin_token_expires_at');
if (!$token) {
return false;
}
if ($expiresAt && now()->greaterThanOrEqualTo($expiresAt)) {
return false;
}
return true;
}
/**
* Get remaining days of Access Token
*/
public function getTokenExpirationStatus(): array
{
$expiresAt = Setting::get('linkedin_token_expires_at');
if (!$expiresAt) {
return [
'is_valid' => false,
'message' => 'Yetkilendirme yapılmadı.',
];
}
$date = \Carbon\Carbon::parse($expiresAt);
$diff = (int) round(now()->diffInDays($date, false));
if ($diff <= 0) {
return [
'is_valid' => false,
'message' => 'Erişim anahtarının süresi doldu (' . $date->format('d.m.Y H:i') . '). Yeniden yetkilendirin.',
];
}
return [
'is_valid' => true,
'days_left' => $diff,
'expires_at' => $date->format('d.m.Y H:i'),
'message' => "Erişim anahtarı aktif. Kalan süre: {$diff} gün ({$date->format('d.m.Y H:i')}).",
];
}
/**
* Share a post to LinkedIn Company Page
*/
public function sharePost(string $title, string $text, ?string $url = null, ?string $imageUrl = null): array
{
if (!$this->isAuthorized()) {
return [
'success' => false,
'message' => 'LinkedIn yetkilendirmesi bulunamadı veya süresi dolmuş.',
];
}
$token = Setting::get('linkedin_access_token');
$authorUrn = "urn:li:organization:{$this->organizationId}";
// Prepare UGC Post payload
$shareCommentary = trim($title . "\n\n" . $text);
if ($url) {
$shareCommentary .= "\n\n" . $url;
}
$mediaContent = [];
if ($url) {
$mediaItem = [
'status' => 'READY',
'originalUrl' => $url,
'title' => [
'text' => Str::limit($title, 200),
],
'description' => [
'text' => Str::limit(strip_tags($text), 250),
],
];
$mediaContent[] = $mediaItem;
}
$shareContent = [
'shareCommentary' => [
'text' => $shareCommentary,
],
'shareMediaCategory' => !empty($mediaContent) ? 'ARTICLE' : 'NONE',
];
if (!empty($mediaContent)) {
$shareContent['media'] = $mediaContent;
}
$payload = [
'author' => $authorUrn,
'lifecycleState' => 'PUBLISHED',
'specificContent' => [
'com.linkedin.ugc.ShareContent' => $shareContent,
],
'visibility' => [
'com.linkedin.ugc.ShareProductVisibility' => 'PUBLIC',
],
];
try {
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $token,
'X-Restli-Protocol-Version' => '2.0.0',
'Content-Type' => 'application/json',
])->post('https://api.linkedin.com/v2/ugcPosts', $payload);
if ($response->successful()) {
$postId = $response->header('x-restli-id') ?? $response->json('id');
Log::info('LinkedIn post published successfully', [
'organization_id' => $this->organizationId,
'post_id' => $postId,
]);
return [
'success' => true,
'message' => 'LinkedIn gönderisi başarıyla paylaşıldı!',
'post_id' => $postId,
];
}
Log::error('LinkedIn Share API Failed', [
'status' => $response->status(),
'body' => $response->body(),
]);
return [
'success' => false,
'message' => 'LinkedIn gönderisi paylaşılamadı: ' . ($response->json('message') ?? $response->body()),
];
} catch (\Throwable $e) {
Log::error('LinkedIn Share Post Exception: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Gönderi paylaşılırken bir hata oluştu: ' . $e->getMessage(),
];
}
}
}
+1 -1
View File
@@ -61,7 +61,7 @@ class BlogStructuredData extends StructuredData
'inLanguage' => app()->getLocale(),
'author' => [
'@type' => 'Person',
'name' => $post->author->name ?? __('blog.default_author'),
'name' => $post->author_name,
],
'publisher' => self::publisherNode(),
'datePublished' => $publishedAt->toIso8601String(),
+37 -1
View File
@@ -63,10 +63,42 @@ abstract class StructuredData
$siteName = static::siteName();
$node = [
'@type' => 'Organization',
'@type' => 'ProfessionalService',
'@id' => $siteUrl . '#organization',
'name' => $siteName,
'url' => $siteUrl,
'priceRange' => '$$',
'geo' => [
'@type' => 'GeoCoordinates',
'latitude' => 37.025704,
'longitude' => 37.296559,
],
'areaServed' => [
[
'@type' => 'AdministrativeArea',
'name' => 'Gaziantep',
],
[
'@type' => 'Country',
'name' => 'Turkey',
]
],
'knowsAbout' => [
'Yazılım Geliştirme',
'Web Tasarım',
'Mobil Uygulama Geliştirme',
'E-Ticaret Sistemleri',
'SEO Danışmanlığı',
'Gaziantep Yazılım Şirketleri'
],
'openingHoursSpecification' => [
[
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
'opens' => '09:00',
'closes' => '18:00',
]
]
];
$logo = setting('site_logo');
@@ -93,6 +125,10 @@ abstract class StructuredData
$node['address'] = [
'@type' => 'PostalAddress',
'streetAddress' => $address,
'addressLocality' => 'Şahinbey',
'addressRegion' => 'Gaziantep',
'postalCode' => '27190',
'addressCountry' => 'TR',
];
}
+19 -2
View File
@@ -45,14 +45,14 @@ trait HasTranslations
// 1. Try translations table
$translation = $this->getTranslation($fieldName, $languageCode);
if ($translation && !empty($translation->field_value)) {
if ($translation && $this->hasValidTranslationContent($translation->field_value)) {
return $translation->field_value;
}
// 2. Fallback to default language from translations table
if ($fallback && $languageCode !== $this->getDefaultLanguageCode()) {
$defaultTranslation = $this->getTranslation($fieldName, $this->getDefaultLanguageCode());
if ($defaultTranslation && !empty($defaultTranslation->field_value)) {
if ($defaultTranslation && $this->hasValidTranslationContent($defaultTranslation->field_value)) {
return $defaultTranslation->field_value;
}
}
@@ -267,6 +267,23 @@ trait HasTranslations
}
}
/**
* Check if translation content has meaningful text or media
*/
protected function hasValidTranslationContent(mixed $value): bool
{
if (empty($value)) {
return false;
}
if (is_string($value)) {
$stripped = trim(strip_tags($value, '<img><iframe><video><audio>'));
return $stripped !== '';
}
return true;
}
/**
* Get translatable fields for this model
*/
Symlink
+1
View File
@@ -0,0 +1 @@
public/b2b
+11
View File
@@ -0,0 +1,11 @@
<?php
return [
'client_id' => env('LINKEDIN_CLIENT_ID'),
'client_secret' => env('LINKEDIN_CLIENT_SECRET'),
'organization_id' => env('LINKEDIN_ORGANIZATION_ID', '35611757'),
'redirect_uri' => env('LINKEDIN_REDIRECT_URI', 'https://truncgil.com/admin/linkedin/callback'),
// Scopes allowed by your LinkedIn App products
'scopes' => explode(' ', env('LINKEDIN_SCOPES', 'w_member_social w_organization_social r_organization_admin rw_organization_admin')),
];
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->boolean('supervisor_approved')->default(false);
$table->boolean('unit_approved')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->dropColumn(['supervisor_approved', 'unit_approved']);
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->boolean('is_retroactive')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->dropColumn('is_retroactive');
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->string('supervisor_name')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('internship_journal_entries', function (Blueprint $table) {
$table->dropColumn('supervisor_name');
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->string('github_username')->nullable()->after('github_repo');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->dropColumn('github_username');
});
}
};
@@ -0,0 +1,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');
}
};
+11
View File
@@ -0,0 +1,11 @@
<?php
$content = file_get_contents('/home/truncgil/web/truncgil.com/public_html/app/Filament/Admin/Pages/LinkedInSettings.php');
$content = str_replace('use Filament\Schemas\Schema;', 'use Filament\Forms\Form;', $content);
$content = str_replace('public function form(Schema $schema): Schema', 'public function form(Form $form): Form', $content);
$content = preg_replace('/return \$schema\n\s*->components/m', 'return $form->schema', $content);
$content = str_replace('use Filament\Schemas\Components\Actions;', 'use Filament\Forms\Components\Actions;', $content);
$content = str_replace('use Filament\Schemas\Components\Actions\Action;', 'use Filament\Forms\Components\Actions\Action;', $content);
$content = str_replace('use Filament\Schemas\Components\Grid;', 'use Filament\Forms\Components\Grid;', $content);
$content = str_replace('use Filament\Schemas\Components\Section;', 'use Filament\Forms\Components\Section;', $content);
file_put_contents('/home/truncgil/web/truncgil.com/public_html/app/Filament/Admin/Pages/LinkedInSettings.php', $content);
echo "Fixed!";
+2
View File
@@ -61,7 +61,9 @@ return [
// Status options
'status_draft' => 'Draft',
'status_pending' => 'Pending Approval',
'status_published' => 'Published',
'status_rejected' => 'Revision Requested',
'status_archived' => 'Archived',
// Messages
+2
View File
@@ -61,7 +61,9 @@ return [
// Status options
'status_draft' => 'Taslak',
'status_pending' => 'Onay Bekliyor',
'status_published' => 'Yayınlandı',
'status_rejected' => 'Revize İstendi',
'status_archived' => 'Arşivlendi',
// Messages
+4 -4
View File
@@ -80,11 +80,11 @@ return [
'intent_letter' => 'Niyet Mektubu',
// Statuses
'pending' => 'Beklemede',
'reviewed' => 'İncelendi',
'pending' => '1. Aşama: Beklemede',
'reviewed' => '2. Aşama: İncelendi',
'waiting_document' => '3. Aşama: Staj Formu Bekleniyor',
'accepted' => '4. Aşama: Kabul Edildi',
'rejected' => 'Reddedildi',
'accepted' => 'Kabul Edildi',
'waiting_document' => 'İmzalı Staj Formu Bekleniyor',
// Wizard and Mermaid
'step_1_title' => '1. Şartlar & Koşullar',
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<circle cx="64" cy="64" r="64" fill="#E2E8F0"/>
<circle cx="64" cy="46" r="22" fill="#94A3B8"/>
<path d="M64 74C42 74 24 88 20 106C31.5 119.6 48.7 128 64 128C79.3 128 96.5 119.6 108 106C104 88 86 74 64 74Z" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

+8
View File
@@ -0,0 +1,8 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /b2b/
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /b2b/index.html [L]
</IfModule>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

+25
View File
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="tr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#e6eef9" />
<title>Trunçgil B2B | Toptancı & Bayi Yönetim Platformu</title>
<meta
name="description"
content="Trunçgil B2B: QR stok, bayi kataloğu, sipariş, iskonto kademesi ve tedarikçi portalı. Toptancılar için kurumsal B2B SaaS."
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="canonical" href="https://truncgil.com/b2b/" />
<script type="module" crossorigin src="/b2b/assets/index-B0gy-q6S.js"></script>
<link rel="stylesheet" crossorigin href="/b2b/assets/index-CIcsclZq.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 708 KiB

+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#1D4ED8"/>
<stop offset="0.55" stop-color="#2563EB"/>
<stop offset="1" stop-color="#22D3EE"/>
</linearGradient>
<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#22D3EE" stop-opacity="0.9"/>
<stop offset="1" stop-color="#67E8F9" stop-opacity="0.5"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="112" fill="url(#bg)"/>
<circle cx="256" cy="256" r="168" fill="none" stroke="url(#ring)" stroke-width="10" opacity="0.85"/>
<text x="256" y="292" text-anchor="middle" font-family="Plus Jakarta Sans, Segoe UI, system-ui, sans-serif" font-size="148" font-weight="800" fill="#FFFFFF" letter-spacing="-4">B2B</text>
</svg>

After

Width:  |  Height:  |  Size: 898 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 595 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /b2b/
Sitemap: https://truncgil.com/b2b/sitemap.xml
Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://truncgil.com/b2b/</loc></url>
<url><loc>https://truncgil.com/b2b/videolar</loc></url>
<url><loc>https://truncgil.com/b2b/kullanim-kilavuzu</loc></url>
<url><loc>https://truncgil.com/b2b/demo</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/toptanci-girisi</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/bayi-girisi</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/yeni-kategori-ekleme</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/yeni-bayi-ekleme</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/depo-ekleme</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/tedarikci-ekleme</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/kullanici-ekleme</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/iskonto-kademesi</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/excel-import-export</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/bayi-siparis-sureci</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/siparis-yonetme</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/plasiyer-mantigi</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/depo-portali-islemler</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/qr-mal-kabul</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/siparis-toplama-picking</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/depolar-arasi-sevk</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/tedarikci-portal-alim</loc></url>
<url><loc>https://truncgil.com/b2b/videolar/yeni-urun-karti</loc></url>
</urlset>
@@ -0,0 +1 @@
google-site-verification: googlef1M6GQLxVA5g7IaVY6kMQCoIrgADR6HjrOZu79CrXYc.html
+9 -1
View File
@@ -1,2 +1,10 @@
User-agent: *
Disallow:
Allow: /
Disallow: /admin/
Disallow: /stajyer/admin/
Disallow: /teklif/
Disallow: /proje-takip/
Disallow: /truncgil-oem-b2b
Disallow: /oem-b2b-demo
Sitemap: https://truncgil.com/sitemap.xml
+3 -3
View File
@@ -37,11 +37,11 @@
<i class="uil uil-calendar-alt pr-[0.2rem] align-[-.05rem] before:content-['\e9ba']"></i>
<span>{{ $cDate }}</span>
</li>
@if($cPost->author)
@if($cPost->author || $cPost->careerApplication)
<li class="post-author inline-block before:content-[''] before:inline-block before:w-[0.2rem] before:h-[0.2rem] before:opacity-50 before:m-[0_.6rem_0] before:rounded-[100%] before:align-[.15rem] before:bg-[#aab0bc]">
<span class="!text-[#aab0bc]">
<i class="uil uil-user pr-[0.2rem] align-[-.05rem] before:content-['\ed6f']"></i>
<span>{{ $cPost->author->name }}</span>
<span>{{ $cPost->author_name }}</span>
</span>
</li>
@endif
@@ -349,7 +349,7 @@
"datePublished" => $p->published_at ? $p->published_at->toIso8601String() : $p->created_at->toIso8601String(),
"author" => [
"@type" => "Person",
"name" => $p->author ? $p->author->name : 'Trunçgil'
"name" => $p->author_name
]
];
}
+8 -8
View File
@@ -22,7 +22,7 @@
<li class="post-author inline-block before:content-[''] before:inline-block before:w-[0.2rem] before:h-[0.2rem] before:opacity-50 before:m-[0_.6rem_0_.4rem] before:rounded-[100%] before:align-[.15rem] before:bg-[#aab0bc]">
<span class="!text-[0.8rem] !text-[#aab0bc]">
<i class="uil uil-user pr-[0.2rem] align-[-.05rem] before:content-['\ed6f']"></i>
<span>{{ $post->author ? $post->author->name : 'Trunçgil' }}</span>
<span>{{ $post->author_name }}</span>
</span>
</li>
</ul>
@@ -84,15 +84,15 @@
<div class="author-info xl:!flex lg:!flex md:!flex items-center !mb-3">
<div class="flex items-center">
<figure class="w-12 h-12 !relative !mr-4 rounded-[100%]">
<img class="rounded-[50%]" alt="image" src="{{ $post->author && $post->author->avatar ? asset('storage/' . $post->author->avatar) : asset('assets/img/avatars/u5.webp') }}" onerror="this.src='{{ asset('assets/img/avatars/u5.webp') }}'" loading="lazy">
<img class="rounded-[50%]" alt="image" src="{{ $post->author_avatar_url }}" onerror="this.src='{{ asset('assets/img/avatars/u5.webp') }}'" loading="lazy">
</figure>
<div>
<h6><a href="#" class="!text-[#343f52] hover:!text-[#e31e24]">{{ $post->author ? $post->author->name : 'Trunçgil' }}</a></h6>
<span class="!text-[0.75rem] !text-[#aab0bc] m-0 p-0 list-none">{{ $post->author && $post->author->role ? $post->author->role : 'Trunçgil Teknoloji Editörü' }}</span>
<h6><a href="#" class="!text-[#343f52] hover:!text-[#e31e24]">{{ $post->author_name }}</a></h6>
<span class="!text-[0.75rem] !text-[#aab0bc] m-0 p-0 list-none">{{ $post->author_role }}</span>
</div>
</div>
<div class="!mt-3 xl:!mt-0 lg:!mt-0 md:!mt-0 !ml-auto">
<a href="{{ route('blog.index', ['author' => $post->author_id]) }}" class="btn btn-sm btn-soft-ash !rounded-[50rem] btn-icon btn-icon-start !mb-0 hover:translate-y-[-0.15rem] hover:shadow-[0_0.25rem_0.75rem_rgba(30,34,40,0.15)]"><i class="uil uil-file-alt !mr-[0.3rem] before:content-['\eaec'] text-[.8rem]"></i> Tüm Yazıları</a>
<a href="{{ $post->author_id ? route('blog.index', ['author' => $post->author_id]) : ($post->career_application_id ? route('blog.index', ['intern' => $post->career_application_id]) : route('blog.index')) }}" class="btn btn-sm btn-soft-ash !rounded-[50rem] btn-icon btn-icon-start !mb-0 hover:translate-y-[-0.15rem] hover:shadow-[0_0.25rem_0.75rem_rgba(30,34,40,0.15)]"><i class="uil uil-file-alt !mr-[0.3rem] before:content-['\eaec'] text-[.8rem]"></i> Tüm Yazıları</a>
</div>
</div>
<p>Trunçgil Teknoloji editörleri tarafından kaleme alınmış bu makalede teknoloji, yazılım geliştirme ve dijital dönüşüm konularına dair güncel gelişmeleri incelediniz. Destek ve sorularınız için bizimle iletişime geçebilirsiniz.</p>
@@ -181,9 +181,9 @@
$publishedDate = $post->published_at ? $post->published_at->toIso8601String() : $post->created_at->toIso8601String();
$modifiedDate = $post->updated_at ? $post->updated_at->toIso8601String() : $publishedDate;
$authorName = $post->author ? $post->author->name : 'Trunçgil';
$authorRole = $post->author && $post->author->role ? $post->author->role : 'Trunçgil Teknoloji Editörü';
$authorUrl = route('blog.index', ['author' => $post->author_id ?? 1]);
$authorName = $post->author_name;
$authorRole = $post->author_role;
$authorUrl = $post->author_id ? route('blog.index', ['author' => $post->author_id]) : ($post->career_application_id ? route('blog.index', ['intern' => $post->career_application_id]) : route('blog.index'));
$siteName = setting('site_name', 'Trunçgil Teknoloji');
$siteLogo = setting('site_logo') ? (str_starts_with(setting('site_logo'), 'http') ? setting('site_logo') : asset('storage/' . ltrim(setting('site_logo'), '/'))) : asset('assets/img/logo.png');
@@ -1,9 +1,9 @@
@php
// Blog modelinden published içerikleri çek
$blogs = \App\Models\Blog::with(['category', 'author'])
$blogs = \App\Models\Blog::with(['category', 'author', 'careerApplication'])
->withCount('comments')
->published()
->latest('published_at')
->orderByRaw('COALESCE(published_at, created_at) DESC')
->limit(6)
->get();
@endphp
@@ -0,0 +1,5 @@
<x-filament-panels::page>
<form wire:submit.prevent="saveSettings">
{{ $this->form }}
</form>
</x-filament-panels::page>
@@ -1,157 +1 @@
<x-filament-widgets::widget>
<!-- DevExtreme Gantt CSS Dependencies -->
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.5/css/dx.light.css">
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.5/css/dx-gantt.css">
<style>
.fi-wi-intern-gantt-chart .demo-container {
height: 560px;
background-color: #fff;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0.05);
margin-top: 12px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
}
.dark .fi-wi-intern-gantt-chart .demo-container {
background-color: #111827;
border-color: rgba(255, 255, 255, 0.05);
}
.fi-wi-intern-gantt-chart #gantt {
height: 100%;
width: 100%;
}
</style>
<x-filament::section class="fi-wi-intern-gantt-chart">
<x-slot name="heading">
<div class="flex items-center justify-between">
<span class="text-lg font-bold tracking-tight text-gray-950 dark:text-white">
Stajyer Gantt Şeması ve Takvimi
</span>
<span class="text-xs px-2.5 py-1 bg-emerald-50 text-emerald-600 dark:bg-emerald-950 dark:text-emerald-400 rounded-full font-semibold">
DevExtreme Görünümü
</span>
</div>
</x-slot>
@if(count($interns) === 0)
<div class="flex flex-col items-center justify-center p-8 text-center bg-gray-50 dark:bg-gray-900/40 rounded-xl border border-dashed border-gray-200 dark:border-gray-800">
<div class="p-3 bg-gray-100 dark:bg-gray-800 rounded-full mb-3 text-gray-400">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" />
</svg>
</div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">
Tarihleri belirlenmiş onaylı stajyer bulunamadı.
</p>
</div>
@else
<div
x-data="{
internsData: @js($interns),
loadScript(src) {
return new Promise((resolve, reject) => {
if (window.jQuery && src.includes('jquery')) { resolve(); return; }
if (window.DevExpress && src.includes('dx.all')) { resolve(); return; }
if (window.DevExpress && window.DevExpress.ui && window.DevExpress.ui.dxGantt && src.includes('dx-gantt')) { resolve(); return; }
const existing = document.querySelector(`script[src='${src}']`);
if (existing) {
if (existing.readyState === 'loaded' || existing.readyState === 'complete') {
resolve();
} else {
existing.addEventListener('load', resolve);
existing.addEventListener('error', reject);
}
return;
}
const script = document.createElement('script');
script.src = src;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('Failed to load script: ' + src));
document.head.appendChild(script);
});
},
showError(msg) {
const errDiv = document.createElement('div');
errDiv.style.color = '#ef4444';
errDiv.style.padding = '20px';
errDiv.style.fontFamily = 'monospace';
errDiv.style.fontSize = '13px';
errDiv.innerHTML = '<strong>Gantt Load Error:</strong><br>' + msg;
this.$refs.ganttContainer.innerHTML = '';
this.$refs.ganttContainer.appendChild(errDiv);
},
async init() {
try {
await this.loadScript('https://code.jquery.com/jquery-3.7.1.min.js').catch(() => { throw new Error('jQuery failed to load'); });
await this.loadScript('https://cdn3.devexpress.com/jslib/23.2.5/js/dx.all.js').catch(() => { throw new Error('dx.all.js failed to load'); });
await this.loadScript('https://cdn3.devexpress.com/jslib/23.2.5/js/dx-gantt.min.js').catch(() => { throw new Error('dx-gantt.min.js failed to load'); });
if (window.jQuery && window.jQuery.fn.dxGantt) {
const formattedData = this.internsData.map(intern => ({
id: intern.id,
parentId: 0,
title: intern.name,
start: new Date(intern.raw_start),
end: new Date(intern.raw_end),
progress: 100
}));
jQuery(this.$refs.ganttContainer).dxGantt({
tasks: {
dataSource: formattedData,
},
editing: {
enabled: false,
},
validation: {
autoUpdateParentTasks: true,
},
toolbar: {
items: [
'collapseAll',
'expandAll',
'separator',
'zoomIn',
'zoomOut',
],
},
columns: [{
dataField: 'title',
caption: 'Stajyer Adı',
width: 250,
}, {
dataField: 'start',
caption: 'Başlangıç Tarihi',
dataType: 'date',
format: 'dd.MM.yyyy',
width: 120,
}, {
dataField: 'end',
caption: 'Bitiş Tarihi',
dataType: 'date',
format: 'dd.MM.yyyy',
width: 120,
}],
scaleType: 'days',
taskListWidth: 490,
});
} else {
throw new Error('DevExpress Gantt component jQuery plugin is not defined after loading scripts.');
}
} catch (e) {
console.error(e);
this.showError(e.message);
}
}
}"
class="dx-viewport demo-container"
>
<div id="gantt" x-ref="ganttContainer" style="height: 100%; width: 100%;"></div>
</div>
@endif
</x-filament::section>
</x-filament-widgets::widget>
{{-- Bu bileşen görünümü devre dışı bırakılmıştır. --}}
@@ -11,15 +11,785 @@
<!-- Statistics Section -->
@include('front.career.partials.stats')
<!-- Gantt Chart Section -->
@include('front.career.partials.gantt')
<!-- Tab Switcher Menu -->
<div class="bg-white rounded-3xl p-4 shadow-xl border border-slate-100/50 mb-8">
<div class="flex flex-col sm:flex-row gap-3" role="tablist">
<button type="button" role="tab" aria-selected="true" data-tab-target="gantt-chart" class="admin-tab-btn active flex-1 flex items-center justify-center gap-3 py-4 px-6 rounded-2xl text-center font-bold text-sm transition-all border border-transparent cursor-pointer">
<i class="uil uil-schedule text-lg"></i>
<span>Gantt Şeması ve Takvimi</span>
</button>
<!-- Interns Table / Detailed List Section -->
<button type="button" role="tab" aria-selected="false" data-tab-target="intern-list" class="admin-tab-btn flex-1 flex items-center justify-center gap-3 py-4 px-6 rounded-2xl text-center font-bold text-sm transition-all border border-transparent text-slate-600 hover:bg-slate-50 cursor-pointer">
<i class="uil uil-list-ul text-lg"></i>
<span>Stajyer Listesi ve Detaylı Takip</span>
</button>
<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>
<!-- Tab Panels -->
<div id="gantt-chart-panel" class="admin-tab-panel space-y-6">
@include('front.career.partials.gantt')
</div>
<div id="intern-list-panel" class="admin-tab-panel hidden space-y-6">
@include('front.career.partials.list')
</div>
<div id="quick-approval-panel" class="admin-tab-panel hidden space-y-6">
@include('front.career.partials.quick_approval')
</div>
</div>
</div>
</section>
@include('front.career.partials.guide_modal')
<!-- Intern Journal Tracking & Approval Modal -->
<div id="intern-journal-modal" class="fixed inset-0 z-[9998] hidden items-center justify-center bg-slate-900/60 backdrop-blur-sm transition-opacity duration-300">
<div class="bg-white rounded-3xl shadow-2xl border border-slate-100 max-w-4xl w-full mx-4 overflow-hidden transform scale-95 opacity-0 transition-all duration-300 flex flex-col max-h-[90vh]">
<!-- Header -->
<div class="px-6 py-4 bg-gradient-to-r from-blue-50 to-indigo-50/30 border-b border-slate-100 flex items-center justify-between flex-shrink-0">
<div>
<h4 id="ijm-name" class="font-bold text-slate-800 text-lg">Stajyer Defteri ve İnceleme</h4>
<p id="ijm-meta" class="text-xs text-slate-500 font-semibold mt-0.5"></p>
</div>
<button type="button" onclick="closeInternJournalModal()" class="text-slate-400 hover:text-slate-600 transition-colors p-1.5 rounded-full hover:bg-slate-100/80">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<!-- Body -->
<div class="flex-grow p-6 overflow-y-auto no-scrollbar grid grid-cols-1 md:grid-cols-12 gap-6">
<!-- Left Sidebar: Progress & General Approvals (col-span-4) -->
<div class="md:col-span-4 space-y-6">
<!-- Progress Bar Card -->
<div class="bg-slate-50 rounded-2xl p-5 border border-slate-200/50">
<h5 class="font-bold text-slate-700 text-xs uppercase tracking-wider mb-4 flex items-center gap-1.5">
<i class="uil uil-chart-bar text-blue-600"></i>
<span>Staj İlerlemesi</span>
</h5>
<!-- Journal Filling Progress -->
<div class="space-y-2 mb-4">
<div class="flex justify-between text-xs font-bold">
<span class="text-slate-500">Defter Doldurma</span>
<span id="ijm-fill-text" class="text-slate-700">0 / 0 Gün</span>
</div>
<div class="w-full bg-slate-200 h-2.5 rounded-full overflow-hidden">
<div id="ijm-fill-progress" class="bg-blue-600 h-full rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
<!-- Journal Approval Progress -->
<div class="space-y-2">
<div class="flex justify-between text-xs font-bold">
<span class="text-slate-500">Onaylı Günler</span>
<span id="ijm-approved-text" class="text-slate-700">0 / 0 Gün</span>
</div>
<div class="w-full bg-slate-200 h-2.5 rounded-full overflow-hidden">
<div id="ijm-approved-progress" class="bg-emerald-500 h-full rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Overall Signatures Card -->
<div class="bg-slate-50 rounded-2xl p-5 border border-slate-200/50 space-y-4">
<h5 class="font-bold text-slate-700 text-xs uppercase tracking-wider flex items-center gap-1.5">
<i class="uil uil-signature text-indigo-600"></i>
<span>Resmi Onaylar / İmzalar</span>
</h5>
<!-- Supervisor Signature -->
<div class="p-3 bg-white rounded-xl border border-slate-100 flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-600">Sorumlu İmzası</span>
<button type="button" id="ijm-btn-supervisor" onclick="toggleNotebookSignature('supervisor')" class="px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border transition-all"></button>
</div>
<div class="text-[10px] text-slate-400 font-semibold" id="ijm-text-supervisor">İmzalanmadı</div>
</div>
<!-- Unit Signature -->
<div class="p-3 bg-white rounded-xl border border-slate-100 flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-600">Birim Sorumlu İmzası</span>
<button type="button" id="ijm-btn-unit" onclick="toggleNotebookSignature('unit')" class="px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border transition-all"></button>
</div>
<div class="text-[10px] text-slate-400 font-semibold" id="ijm-text-unit">İmzalanmadı</div>
</div>
<!-- Notebook Approved -->
<div class="p-3 bg-white rounded-xl border border-slate-100 flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-600">Genel Defter Onayı</span>
<button type="button" id="ijm-btn-approved" onclick="toggleNotebookSignature('approved')" class="px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border transition-all"></button>
</div>
<div class="text-[10px] text-slate-400 font-semibold" id="ijm-text-approved">Onaylanmadı</div>
</div>
</div>
<!-- Official Documents & Quick Exports Card -->
<div class="bg-slate-50 rounded-2xl p-5 border border-slate-200/50 space-y-3">
<h5 class="font-bold text-slate-700 text-xs uppercase tracking-wider flex items-center gap-1.5">
<i class="uil uil-file-download-alt text-emerald-600"></i>
<span>Resmi Belgeler & Çıktılar</span>
</h5>
<div class="space-y-2" id="ijm-documents-container">
<a href="#" id="ijm-btn-verify-cert" target="_blank" class="btn-doc-emerald w-full px-3 py-2 bg-emerald-600 hover:bg-emerald-700 text-white hover:text-white font-extrabold text-xs rounded-xl transition-all flex items-center justify-between shadow-sm">
<span class="flex items-center gap-1.5"><i class="uil uil-award"></i> Sertifika & Transkript</span>
<i class="uil uil-external-link-alt text-xs"></i>
</a>
<div class="grid grid-cols-2 gap-2">
<a href="#" id="ijm-btn-print-a4" target="_blank" class="btn-doc-slate px-3 py-2 bg-white hover:bg-slate-100 border border-slate-200 text-slate-700 hover:text-slate-900 font-bold text-xs rounded-xl transition-all flex items-center justify-center gap-1">
<i class="uil uil-print text-xs"></i> A4 Defter
</a>
<a href="#" id="ijm-btn-print-a5" target="_blank" class="btn-doc-slate px-3 py-2 bg-white hover:bg-slate-100 border border-slate-200 text-slate-700 hover:text-slate-900 font-bold text-xs rounded-xl transition-all flex items-center justify-center gap-1">
<i class="uil uil-print text-xs"></i> A5 Defter
</a>
</div>
<a href="#" id="ijm-btn-download-md" target="_blank" class="btn-doc-slate w-full px-3 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 hover:text-slate-900 font-bold text-xs rounded-xl transition-all flex items-center justify-center gap-1.5">
<i class="uil uil-arrow-down-tray"></i> Günlüğü İndir (.md)
</a>
<a href="#" id="ijm-btn-repo" target="_blank" class="hidden w-full px-3 py-2 bg-slate-900 hover:bg-black text-white hover:text-white font-bold text-xs rounded-xl transition-all items-center justify-center gap-1.5">
<i class="uil uil-github"></i> GitHub Reposu
</a>
</div>
</div>
</div>
<!-- Right Section: Carousel & Navigation (col-span-8) -->
<div class="md:col-span-8 flex flex-col space-y-4">
<!-- Carousel Controls -->
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-200/50 flex items-center justify-between gap-3">
<button type="button" onclick="prevSlide()" class="p-2 bg-white hover:bg-slate-100 border border-slate-200 text-slate-600 hover:text-slate-900 rounded-xl transition-all flex items-center justify-center cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"/></svg>
</button>
<div class="flex-grow flex items-center justify-center gap-3">
<span id="ijm-day-indicator" class="text-sm font-extrabold text-blue-600 bg-blue-50 px-3 py-1.5 rounded-xl">Gün: 0 / 0</span>
<!-- Quick Day Dropdown -->
<select id="ijm-day-select" onchange="goToSlide(this.value)" class="text-xs font-semibold text-slate-700 bg-white border border-slate-200 rounded-xl px-2.5 py-1.5 focus:outline-none focus:border-blue-400">
<!-- Options loaded dynamically -->
</select>
</div>
<button type="button" onclick="nextSlide()" class="p-2 bg-white hover:bg-slate-100 border border-slate-200 text-slate-600 hover:text-slate-900 rounded-xl transition-all flex items-center justify-center cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
</button>
</div>
<!-- Carousel Slide Container -->
<div class="bg-white rounded-2xl border border-slate-100 p-6 flex-grow flex flex-col shadow-sm min-h-[300px]">
<!-- Day Title & Date -->
<div class="flex items-center justify-between pb-3 border-b border-slate-100 mb-4 flex-shrink-0">
<div>
<span id="ijm-slide-title" class="text-sm font-extrabold text-slate-800">Seçili Gün</span>
<span id="ijm-slide-date" class="text-xs font-semibold text-slate-400 ml-2"></span>
</div>
<div id="ijm-slide-retroactive">
<!-- Retroactive badge -->
</div>
</div>
<!-- Slide Content (Rich text HTML) -->
<div class="flex-grow overflow-y-auto no-scrollbar prose max-w-none text-slate-700 leading-relaxed text-sm" style="max-height: 250px;">
<div id="ijm-slide-content">
<!-- Day content loaded here -->
</div>
</div>
<!-- Slide Actions / Daily approval status -->
<div class="pt-4 border-t border-slate-100 flex items-center justify-between mt-4 flex-shrink-0">
<div id="ijm-slide-status-badge">
<!-- Status badge -->
</div>
<button type="button" id="ijm-slide-action-btn" onclick="toggleActiveDayApproval()" class="px-4 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-xl font-bold text-xs shadow-lg shadow-blue-500/20 transition-all cursor-pointer">
Onayla
</button>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 bg-slate-50 border-t border-slate-100 flex items-center justify-between flex-shrink-0">
<div class="flex items-center gap-2">
<a href="#" id="ijm-footer-verify" target="_blank" class="btn-doc-emerald px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white hover:text-white font-bold rounded-xl text-xs transition-colors shadow-sm flex items-center gap-1.5">
<i class="uil uil-award"></i>
<span>Sertifika & Transkript Doğrula</span>
</a>
</div>
<button type="button" onclick="closeInternJournalModal()" class="px-5 py-2.5 bg-slate-800 hover:bg-slate-900 text-white font-bold rounded-xl text-xs transition-colors shadow-md cursor-pointer">Kapat</button>
</div>
</div>
</div>
@push('styles')
<style>
.admin-tab-btn {
transition: all 0.2s ease-in-out;
}
.admin-tab-btn:hover:not(.active) {
background-color: #f8fafc;
}
.admin-tab-btn.active {
background-color: #eff6ff;
border-color: #dbeafe;
color: #1e40af !important;
}
/* Prevent theme red text color on link buttons hover */
a.btn-doc-emerald, .btn-doc-emerald {
color: #ffffff !important;
}
a.btn-doc-emerald:hover, a.btn-doc-emerald:focus, .btn-doc-emerald:hover {
color: #ffffff !important;
background-color: #047857 !important;
text-decoration: none !important;
}
a.btn-doc-blue, .btn-doc-blue {
color: #1d4ed8 !important;
}
a.btn-doc-blue:hover, a.btn-doc-blue:focus, .btn-doc-blue:hover {
color: #1e40af !important;
background-color: #dbeafe !important;
text-decoration: none !important;
}
a.btn-doc-slate, .btn-doc-slate {
color: #475569 !important;
}
a.btn-doc-slate:hover, a.btn-doc-slate:focus, .btn-doc-slate:hover {
color: #0f172a !important;
background-color: #e2e8f0 !important;
text-decoration: none !important;
}
a.btn-doc-amber, .btn-doc-amber {
color: #b45309 !important;
}
a.btn-doc-amber:hover, a.btn-doc-amber:focus, .btn-doc-amber:hover {
color: #92400e !important;
background-color: #fef3c7 !important;
text-decoration: none !important;
}
a.btn-doc-green, .btn-doc-green {
color: #15803d !important;
}
a.btn-doc-green:hover, a.btn-doc-green:focus, .btn-doc-green:hover {
color: #166534 !important;
background-color: #dcfce7 !important;
text-decoration: none !important;
}
a.btn-doc-repo, .btn-doc-repo {
color: #2563eb !important;
}
a.btn-doc-repo:hover, a.btn-doc-repo:focus, .btn-doc-repo:hover {
color: #1d4ed8 !important;
text-decoration: underline !important;
}
a.btn-doc-md, .btn-doc-md {
color: #059669 !important;
}
a.btn-doc-md:hover, a.btn-doc-md:focus, .btn-doc-md:hover {
color: #047857 !important;
background-color: #d1fae5 !important;
text-decoration: none !important;
}
</style>
@endpush
@push('scripts')
<script>
let currentInternId = null;
let journalEntries = [];
let activeSlideIndex = 0;
let isUpdatingSignature = false;
let isUpdatingDayApproval = false;
function openInternJournalModal(internId) {
currentInternId = internId;
activeSlideIndex = 0;
const modal = document.getElementById('intern-journal-modal');
if (!modal) return;
modal.classList.remove('hidden');
modal.classList.add('flex');
// Animate open
const card = modal.querySelector('.max-w-4xl');
if (card) {
setTimeout(() => {
card.classList.remove('scale-95', 'opacity-0');
card.classList.add('scale-100', 'opacity-100');
}, 50);
}
loadInternJournalDetails(internId);
}
function closeInternJournalModal() {
const modal = document.getElementById('intern-journal-modal');
if (!modal) return;
const card = modal.querySelector('.max-w-4xl');
if (card) {
card.classList.remove('scale-100', 'opacity-100');
card.classList.add('scale-95', 'opacity-0');
}
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
}, 200);
}
function loadInternJournalDetails(internId) {
const slideContent = document.getElementById('ijm-slide-content');
if (slideContent) {
slideContent.innerHTML = `
<div class="flex items-center justify-center p-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
`;
}
fetch(`/stajyer/admin/journal-details?intern_id=${internId}`)
.then(res => res.json())
.then(data => {
if (!data.success) {
alert(data.message || 'Veriler yüklenemedi.');
closeInternJournalModal();
return;
}
// Set Header details
const nameEl = document.getElementById('ijm-name');
if (nameEl) nameEl.textContent = data.intern.name;
let startF = data.intern.start_date ? formatDateStr(data.intern.start_date) : '-';
let endF = data.intern.end_date ? formatDateStr(data.intern.end_date) : '-';
const metaEl = document.getElementById('ijm-meta');
if (metaEl) metaEl.textContent = `${startF} - ${endF} (${data.intern.total_days} İş Günü)`;
// Setup Progress Bars
const fillPercent = data.intern.total_days > 0 ? (data.intern.filled_days / data.intern.total_days) * 100 : 0;
const fillTextEl = document.getElementById('ijm-fill-text');
if (fillTextEl) fillTextEl.textContent = `${data.intern.filled_days} / ${data.intern.total_days} Gün`;
const fillBarEl = document.getElementById('ijm-fill-progress');
if (fillBarEl) fillBarEl.style.width = `${fillPercent}%`;
// Approved days count
let approvedDaysCount = 0;
data.entries.forEach(e => {
if (e.filled && e.supervisor_approved) {
approvedDaysCount++;
}
});
const approvedPercent = data.intern.total_days > 0 ? (approvedDaysCount / data.intern.total_days) * 100 : 0;
const appTextEl = document.getElementById('ijm-approved-text');
if (appTextEl) appTextEl.textContent = `${approvedDaysCount} / ${data.intern.total_days} Gün`;
const appBarEl = document.getElementById('ijm-approved-progress');
if (appBarEl) appBarEl.style.width = `${approvedPercent}%`;
// Setup Signatures
updateSignatureUI('supervisor', data.intern.notebook_supervisor_signed, data.intern.notebook_supervisor_name);
updateSignatureUI('unit', data.intern.notebook_unit_signed, data.intern.notebook_unit_name);
updateSignatureUI('approved', data.intern.notebook_approved, null);
// Setup Official Document URLs
const certCode = data.intern.certificate_code;
const certBtn = document.getElementById('ijm-btn-verify-cert');
const footerCertBtn = document.getElementById('ijm-footer-verify');
if (certCode) {
const verifyUrl = `/staj-dogrulama/${certCode}`;
if (certBtn) {
certBtn.href = verifyUrl;
certBtn.classList.remove('hidden');
}
if (footerCertBtn) {
footerCertBtn.href = verifyUrl;
footerCertBtn.classList.remove('hidden');
}
} else {
if (certBtn) certBtn.classList.add('hidden');
if (footerCertBtn) footerCertBtn.classList.add('hidden');
}
const printA4Btn = document.getElementById('ijm-btn-print-a4');
if (printA4Btn) printA4Btn.href = `/stajyer/defteri-yazdir?size=a4&intern_id=${internId}`;
const printA5Btn = document.getElementById('ijm-btn-print-a5');
if (printA5Btn) printA5Btn.href = `/stajyer/defteri-yazdir?size=a5&intern_id=${internId}`;
const mdBtn = document.getElementById('ijm-btn-download-md');
if (mdBtn) mdBtn.href = `/stajyer/gunluk-indir?intern_id=${internId}`;
const repoBtn = document.getElementById('ijm-btn-repo');
if (repoBtn) {
if (data.intern.github_repo) {
repoBtn.href = data.intern.github_repo;
repoBtn.classList.remove('hidden');
repoBtn.classList.add('flex');
} else {
repoBtn.classList.add('hidden');
repoBtn.classList.remove('flex');
}
}
// Setup carousel slide data
journalEntries = data.entries;
// Setup dropdown
const select = document.getElementById('ijm-day-select');
if (select) {
select.innerHTML = '';
journalEntries.forEach((entry, idx) => {
const opt = document.createElement('option');
opt.value = idx;
opt.textContent = `${entry.day_number}. Gün (${entry.formatted_date})` + (entry.filled ? ' [DOLU]' : ' [BOŞ]');
select.appendChild(opt);
});
}
// Set total indicator text
const indEl = document.getElementById('ijm-day-indicator');
if (indEl) indEl.textContent = `Gün: 1 / ${journalEntries.length}`;
// Render first slide
renderSlide(0);
})
.catch(err => {
console.error(err);
alert('Staj detayları yüklenirken bir hata oluştu.');
closeInternJournalModal();
});
}
function formatDateStr(dateStr) {
if (!dateStr) return '';
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[2]}.${parts[1]}.${parts[0]}`;
}
return dateStr;
}
function updateSignatureUI(type, signed, name) {
const btn = document.getElementById(`ijm-btn-${type}`);
const label = document.getElementById(`ijm-text-${type}`);
if (!btn || !label) return;
if (type === 'supervisor') {
if (signed) {
btn.textContent = "İmzayı Kaldır";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-rose-200 bg-rose-50 text-rose-600 hover:bg-rose-100 transition-all cursor-pointer";
label.innerHTML = `<span class="text-green-600 font-extrabold"><i class="uil uil-check-circle mr-0.5"></i>İmzaladı: ${name || ''}</span>`;
} else {
btn.textContent = "İmzala";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-indigo-200 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 transition-all cursor-pointer";
label.textContent = "İmzalanmadı";
}
} else if (type === 'unit') {
if (signed) {
btn.textContent = "İmzayı Kaldır";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-rose-200 bg-rose-50 text-rose-600 hover:bg-rose-100 transition-all cursor-pointer";
label.innerHTML = `<span class="text-green-600 font-extrabold"><i class="uil uil-check-circle mr-0.5"></i>İmzaladı: ${name || ''}</span>`;
} else {
btn.textContent = "İmzala";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-indigo-200 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 transition-all cursor-pointer";
label.textContent = "İmzalanmadı";
}
} else if (type === 'approved') {
if (signed) {
btn.textContent = "Onayı Kaldır";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-rose-200 bg-rose-50 text-rose-600 hover:bg-rose-100 transition-all cursor-pointer";
label.innerHTML = `<span class="text-green-600 font-extrabold"><i class="uil uil-check-circle mr-0.5"></i>Defter Onaylandı</span>`;
} else {
btn.textContent = "Onayla";
btn.className = "px-2.5 py-1 text-[10px] font-extrabold uppercase rounded-lg border border-emerald-200 bg-emerald-50 text-emerald-600 hover:bg-emerald-100 transition-all cursor-pointer";
label.textContent = "Onaylanmadı";
}
}
}
function toggleNotebookSignature(type) {
if (isUpdatingSignature) return;
// Get current state
const btn = document.getElementById(`ijm-btn-${type}`);
if (!btn) return;
const isSignedCurrently = btn.textContent.includes('Kaldır');
const newSignState = !isSignedCurrently;
let promptName = '';
if (newSignState && (type === 'supervisor' || type === 'unit')) {
promptName = prompt("İmzalayan yetkili ismini giriniz veya onaylayınız:", @json(auth()->user()->name));
if (promptName === null) return; // cancelled
if (promptName.trim() === '') {
promptName = @json(auth()->user()->name);
}
}
isUpdatingSignature = true;
btn.style.opacity = '0.5';
fetch('/stajyer/admin/toggle-notebook-signature', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
intern_id: currentInternId,
type: type,
signed: newSignState ? 1 : 0,
name: promptName
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
const key = type === 'supervisor' ? 'notebook_supervisor_signed' : (type === 'unit' ? 'notebook_unit_signed' : 'notebook_approved');
const nameKey = type === 'supervisor' ? 'notebook_supervisor_name' : (type === 'unit' ? 'notebook_unit_name' : null);
updateSignatureUI(type, data.intern[key], data.intern[nameKey]);
} else {
alert(data.message || 'Hata oluştu.');
}
})
.catch(err => {
console.error(err);
alert('İşlem gerçekleştirilemedi.');
})
.finally(() => {
isUpdatingSignature = false;
btn.style.opacity = '1';
});
}
function renderSlide(index) {
if (index < 0 || index >= journalEntries.length) return;
activeSlideIndex = index;
const entry = journalEntries[index];
// Update indicator & select dropdown
const indEl = document.getElementById('ijm-day-indicator');
if (indEl) indEl.textContent = `Gün: ${index + 1} / ${journalEntries.length}`;
const selEl = document.getElementById('ijm-day-select');
if (selEl) selEl.value = index;
// Title & Date
const titleEl = document.getElementById('ijm-slide-title');
if (titleEl) titleEl.textContent = `${entry.day_number}. Gün Raporu`;
const dateEl = document.getElementById('ijm-slide-date');
if (dateEl) dateEl.textContent = entry.formatted_date;
// Retroactive badge
const retroBadge = document.getElementById('ijm-slide-retroactive');
if (retroBadge) {
retroBadge.innerHTML = '';
if (entry.filled && entry.is_retroactive) {
retroBadge.innerHTML = `
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-rose-50 text-rose-700 text-[10px] font-extrabold uppercase border border-rose-100">
<span class="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse"></span>
Geriye Dönük
</span>
`;
}
}
// Content area
const contentArea = document.getElementById('ijm-slide-content');
const badgeDiv = document.getElementById('ijm-slide-status-badge');
const actionBtn = document.getElementById('ijm-slide-action-btn');
if (!contentArea || !badgeDiv || !actionBtn) return;
if (!entry.filled) {
contentArea.innerHTML = `
<div class="flex flex-col items-center justify-center p-8 text-center bg-slate-50 rounded-2xl border border-dashed border-slate-200">
<i class="uil uil-file-slash text-3xl text-slate-400 mb-2"></i>
<p class="text-sm font-medium text-slate-500">Bu gün için henüz staj raporu yazılmamıştır.</p>
</div>
`;
badgeDiv.innerHTML = `<span class="text-xs text-slate-400 font-semibold">Boş Rapor</span>`;
actionBtn.classList.add('hidden');
} else {
actionBtn.classList.remove('hidden');
const entryContent = entry.content || '<p class="text-slate-400 italic">Boş içerik.</p>';
contentArea.innerHTML = `<div class="rich-text-content prose max-w-none text-slate-700 leading-relaxed text-sm select-text">${entryContent}</div>`;
updateDayApprovalUI(entry.supervisor_approved, entry.supervisor_name);
}
}
function updateDayApprovalUI(approved, name) {
const badgeDiv = document.getElementById('ijm-slide-status-badge');
const actionBtn = document.getElementById('ijm-slide-action-btn');
if (!badgeDiv || !actionBtn) return;
if (approved) {
badgeDiv.innerHTML = `
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-50 text-emerald-700 text-xs font-bold border border-emerald-100">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
Sorumlu Onayladı ${name ? `(${name})` : ''}
</span>
`;
actionBtn.textContent = "Onayı Kaldır";
actionBtn.className = "px-4 py-2 text-white bg-rose-600 hover:bg-rose-700 rounded-xl font-bold text-xs shadow-lg shadow-rose-500/20 transition-all cursor-pointer";
} else {
badgeDiv.innerHTML = `
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-slate-100 text-slate-600 text-xs font-bold border border-slate-200">
Onay Bekliyor
</span>
`;
actionBtn.textContent = "Günü Onayla";
actionBtn.className = "px-4 py-2 text-white bg-emerald-600 hover:bg-emerald-700 rounded-xl font-bold text-xs shadow-lg shadow-emerald-500/20 transition-all cursor-pointer";
}
}
function toggleActiveDayApproval() {
if (isUpdatingDayApproval) return;
const entry = journalEntries[activeSlideIndex];
if (!entry || !entry.entry_id) return;
const actionBtn = document.getElementById('ijm-slide-action-btn');
if (!actionBtn) return;
isUpdatingDayApproval = true;
actionBtn.style.opacity = '0.5';
fetch('/stajyer/admin/toggle-approval', {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({
entry_id: entry.entry_id
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
// Update local object
entry.supervisor_approved = data.status;
entry.supervisor_name = data.supervisor_name;
// Update UI
updateDayApprovalUI(data.status, data.supervisor_name);
// Recalculate and update approved progress bar
let approvedDaysCount = 0;
journalEntries.forEach(e => {
if (e.filled && e.supervisor_approved) {
approvedDaysCount++;
}
});
const approvedPercent = journalEntries.length > 0 ? (approvedDaysCount / journalEntries.length) * 100 : 0;
const appTextEl = document.getElementById('ijm-approved-text');
if (appTextEl) appTextEl.textContent = `${approvedDaysCount} / ${journalEntries.length} Gün`;
const appBarEl = document.getElementById('ijm-approved-progress');
if (appBarEl) appBarEl.style.width = `${approvedPercent}%`;
} else {
alert(data.message || "Onay güncellenemedi.");
}
})
.catch(err => {
console.error(err);
alert("İşlem sırasında bir hata oluştu.");
})
.finally(() => {
isUpdatingDayApproval = false;
actionBtn.style.opacity = '1';
});
}
function prevSlide() {
if (activeSlideIndex > 0) {
renderSlide(activeSlideIndex - 1);
}
}
function nextSlide() {
if (activeSlideIndex < journalEntries.length - 1) {
renderSlide(activeSlideIndex + 1);
}
}
function goToSlide(idx) {
renderSlide(parseInt(idx));
}
// Admin Tab Switch Logic
document.addEventListener('DOMContentLoaded', function() {
const adminTabButtons = document.querySelectorAll('.admin-tab-btn');
const adminTabPanels = document.querySelectorAll('.admin-tab-panel');
adminTabButtons.forEach(button => {
button.addEventListener('click', () => {
// Deactivate all buttons
adminTabButtons.forEach(btn => {
btn.classList.remove('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
btn.classList.add('text-slate-600', 'bg-transparent', 'border-transparent');
btn.setAttribute('aria-selected', 'false');
});
// Hide all panels
adminTabPanels.forEach(panel => {
panel.classList.add('hidden');
});
// Activate clicked button
button.classList.add('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
button.classList.remove('text-slate-600', 'bg-transparent', 'border-transparent');
button.setAttribute('aria-selected', 'true');
// Show target panel
const targetId = button.getAttribute('data-tab-target');
const targetPanel = document.getElementById(targetId + '-panel');
if (targetPanel) {
targetPanel.classList.remove('hidden');
// Re-render DevExtreme Gantt chart if visible, to prevent rendering scale bugs
if (targetId === 'gantt-chart') {
const ganttEl = document.getElementById('gantt');
if (ganttEl) {
const ganttInstance = $(ganttEl).dxGantt('instance');
if (ganttInstance) {
ganttInstance.repaint();
}
}
}
if (targetId === 'quick-approval') {
if (typeof loadQuickApprovalEntries === 'function') {
loadQuickApprovalEntries();
}
}
}
});
});
});
</script>
@endpush
@endsection
File diff suppressed because it is too large Load Diff
@@ -21,6 +21,40 @@
@endif
</div>
<!-- Dynamic Journal Modal -->
<div id="journal-modal" class="fixed inset-0 z-[9999] hidden items-center justify-center bg-slate-900/60 backdrop-blur-sm transition-opacity duration-300">
<div class="bg-white rounded-3xl shadow-2xl border border-slate-100 max-w-2xl w-full mx-4 overflow-hidden transform scale-95 opacity-0 transition-all duration-300 flex flex-col max-h-[90vh]">
<!-- Modal Header -->
<div class="px-6 py-4 bg-gradient-to-r from-blue-50 to-indigo-50/30 border-b border-slate-100 flex items-center justify-between">
<div>
<h4 id="modal-title" class="font-bold text-slate-800 text-lg">Staj Raporu Detayı</h4>
<p id="modal-subtitle" class="text-xs text-slate-500 font-semibold mt-0.5"></p>
</div>
<button type="button" onclick="closeJournalModal()" class="text-slate-400 hover:text-slate-600 transition-colors p-1.5 rounded-full hover:bg-slate-100/80">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<!-- Modal Body -->
<div class="p-6 overflow-y-auto flex-grow prose max-w-none text-slate-700 leading-relaxed text-sm no-scrollbar">
<div id="modal-content" class="min-h-[100px] flex flex-col justify-center">
<!-- Content gets loaded here -->
</div>
</div>
<!-- Modal Footer -->
<div class="px-6 py-4 bg-slate-50/50 border-t border-slate-100 flex items-center justify-between">
<div id="modal-status-badge">
<!-- Status badge -->
</div>
<div class="flex gap-3">
<button type="button" onclick="closeJournalModal()" class="px-4 py-2 border border-slate-200 text-slate-600 hover:bg-slate-50 rounded-xl font-bold text-xs transition-colors">Kapat</button>
<button type="button" id="modal-action-btn" onclick="handleModalAction()" class="hidden px-4 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-xl font-bold text-xs shadow-lg shadow-blue-500/20 transition-all"></button>
</div>
</div>
</div>
</div>
@push('styles')
<!-- DevExtreme Gantt CSS Dependencies -->
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.5/css/dx.fluent.blue.light.css">
@@ -31,6 +65,12 @@
border-left: 2px dashed #ef4444 !important;
width: 2px !important;
}
.rich-text-content p { margin-bottom: 8px; }
.rich-text-content ul { list-style-type: disc !important; padding-left: 20px !important; margin-bottom: 8px !important; }
.rich-text-content ol { list-style-type: decimal !important; padding-left: 20px !important; margin-bottom: 8px !important; }
.rich-text-content li { margin-bottom: 4px !important; }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
<script>
tailwind = {
@@ -62,6 +102,8 @@
progress: 0
}));
let dayColumnsMap = {};
$('#gantt').dxGantt({
tasks: {
dataSource: formattedData,
@@ -105,8 +147,192 @@
start: new Date(),
cssClass: 'current-time-line'
}],
onScaleCellPrepared: function(e) {
if (e.scaleType === 'days') {
const $el = $(e.scaleElement || e.element);
const left = $el.position().left;
const width = $el.outerWidth();
const dateStr = e.startDate.toISOString().substring(0, 10);
dayColumnsMap[dateStr] = {
left: left,
right: left + width,
date: e.startDate,
dateStr: dateStr
};
}
}
});
// Grid task area cell click listener
$(document).on('click', '#gantt', function(e) {
const $target = $(e.target);
if ($target.closest('.dx-gantt-task-area').length > 0 || $target.closest('.dx-gantt-ts-area').length > 0) {
const ganttInstance = $('#gantt').dxGantt('instance');
const selectedKey = ganttInstance.option("selectedRowKey");
if (!selectedKey) {
alert("Lütfen önce sol taraftan stajyeri seçin, ardından tıklamak istediğiniz güne tıklayın.");
return;
}
const $taskArea = $('.dx-gantt-task-area').first();
if (!$taskArea.length) return;
const scrollLeft = ganttInstance._ganttView._taskAreaContainer.scrollLeft;
const clickX = e.pageX - $taskArea.offset().left;
const totalX = scrollLeft + clickX;
const cols = Object.values(dayColumnsMap);
const clickedCol = cols.find(col => totalX >= col.left && totalX <= col.right);
if (clickedCol) {
showDailyJournalPopup(selectedKey, clickedCol.dateStr);
}
}
});
});
let activeEntryId = null;
function showDailyJournalPopup(internId, dateStr) {
const modal = document.getElementById('journal-modal');
const contentDiv = document.getElementById('modal-content');
const titleH = document.getElementById('modal-title');
const subtitleP = document.getElementById('modal-subtitle');
const badgeDiv = document.getElementById('modal-status-badge');
const actionBtn = document.getElementById('modal-action-btn');
modal.classList.remove('hidden');
modal.classList.add('flex');
setTimeout(() => {
modal.firstElementChild.classList.remove('scale-95', 'opacity-0');
modal.firstElementChild.classList.add('scale-100', 'opacity-100');
}, 50);
contentDiv.innerHTML = `
<div class="flex items-center justify-center p-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
`;
titleH.textContent = "Yükleniyor...";
subtitleP.textContent = "";
badgeDiv.innerHTML = "";
actionBtn.classList.add('hidden');
activeEntryId = null;
fetch(`/stajyer/admin/journal-entry?intern_id=${internId}&date=${dateStr}`)
.then(res => res.json())
.then(data => {
if (!data.success) {
contentDiv.innerHTML = `<p class="text-slate-500 italic text-center p-8">${data.message}</p>`;
titleH.textContent = "Bilgi";
return;
}
titleH.textContent = `${data.intern_name}`;
subtitleP.textContent = `${data.day_number}. Gün Raporu — ${data.date_formatted}`;
if (!data.entry) {
contentDiv.innerHTML = `
<div class="flex flex-col items-center justify-center p-8 text-center bg-slate-50 rounded-2xl border border-dashed border-slate-200">
<i class="uil uil-file-slash text-3xl text-slate-400 mb-2"></i>
<p class="text-sm font-medium text-slate-500">Bu gün için henüz staj raporu yazılmamıştır.</p>
</div>
`;
return;
}
activeEntryId = data.entry.id;
const entryContent = data.entry.content || '<p class="text-slate-400 italic">Boş içerik.</p>';
let contentHtml = '';
if (data.entry.is_retroactive) {
contentHtml += `
<div class="mb-4 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-rose-50 text-rose-700 text-[10px] font-extrabold uppercase border border-rose-100">
<span class="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse"></span>
Geriye Dönük Kayıt
</div>
`;
}
contentHtml += `<div class="rich-text-content prose max-w-none text-slate-700 leading-relaxed text-sm select-text">${entryContent}</div>`;
contentDiv.innerHTML = contentHtml;
updateModalStatus(data.entry.supervisor_approved, data.entry.supervisor_name);
})
.catch(err => {
console.error(err);
contentDiv.innerHTML = `<p class="text-red-500 text-center p-8 font-semibold">Veriler yüklenirken bir hata oluştu.</p>`;
titleH.textContent = "Hata";
});
}
function updateModalStatus(approved, name) {
const badgeDiv = document.getElementById('modal-status-badge');
const actionBtn = document.getElementById('modal-action-btn');
actionBtn.classList.remove('hidden');
if (approved) {
badgeDiv.innerHTML = `
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-50 text-emerald-700 text-xs font-bold border border-emerald-100">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
Sorumlu Onayladı ${name ? `(${name})` : ''}
</span>
`;
actionBtn.textContent = "Onayı Kaldır";
actionBtn.className = "px-4 py-2 text-white bg-rose-600 hover:bg-rose-700 rounded-xl font-bold text-xs shadow-lg shadow-rose-500/20 transition-all cursor-pointer";
} else {
badgeDiv.innerHTML = `
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-slate-100 text-slate-600 text-xs font-bold border border-slate-200">
Onay Bekliyor
</span>
`;
actionBtn.textContent = "Raporu Onayla";
actionBtn.className = "px-4 py-2 text-white bg-emerald-600 hover:bg-emerald-700 rounded-xl font-bold text-xs shadow-lg shadow-emerald-500/20 transition-all cursor-pointer";
}
}
function handleModalAction() {
if (!activeEntryId) return;
const actionBtn = document.getElementById('modal-action-btn');
actionBtn.disabled = true;
actionBtn.style.opacity = "0.5";
fetch('/stajyer/admin/toggle-approval', {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({
entry_id: activeEntryId
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
updateModalStatus(data.status, data.supervisor_name);
} else {
alert(data.message || "Onay güncellenemedi.");
}
})
.catch(err => {
console.error(err);
alert("İşlem sırasında bir hata oluştu.");
})
.finally(() => {
actionBtn.disabled = false;
actionBtn.style.opacity = "1";
});
}
function closeJournalModal() {
const modal = document.getElementById('journal-modal');
modal.firstElementChild.classList.add('scale-95', 'opacity-0');
modal.firstElementChild.classList.remove('scale-100', 'opacity-100');
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
}, 200);
}
</script>
@endif
@endpush
@@ -11,6 +11,7 @@
<tr class="border-b border-slate-100 text-xs font-extrabold uppercase tracking-wider text-slate-400">
<th class="pb-4">Stajyer</th>
<th class="pb-4">Staj Tarihleri</th>
<th class="pb-4">Defter İlerlemesi</th>
<th class="pb-4">Durum</th>
<th class="pb-4">Belgeler</th>
<th class="pb-4">GitHub & Repo</th>
@@ -22,7 +23,10 @@
<tr class="hover:bg-slate-50/50 transition-colors">
<!-- Name & Info -->
<td class="py-4">
<div class="font-bold text-slate-800 text-sm">{{ $intern->name }}</div>
<div class="font-bold text-slate-800 text-sm cursor-pointer hover:underline hover:text-blue-600 flex items-center gap-1.5" onclick="openInternJournalModal({{ $intern->id }})">
<span>{{ $intern->name }}</span>
<span class="inline-flex px-1.5 py-0.5 rounded-md bg-blue-50 text-blue-600 text-[9px] font-extrabold uppercase border border-blue-100 tracking-wider">Defteri İncele</span>
</div>
<div class="text-xs text-slate-400 mt-0.5 flex flex-col gap-0.5">
<span><i class="uil uil-envelope mr-1"></i>{{ $intern->email }}</span>
@if($intern->phone)
@@ -43,6 +47,30 @@
@endif
</td>
<!-- Defter İlerlemesi -->
<td class="py-4">
@if($intern->internship_total_days)
@php
$filledDays = $intern->journalEntries->filter(function($entry) {
return !empty(trim($entry->content));
})->count();
$progressPercent = ($filledDays / $intern->internship_total_days) * 100;
if ($progressPercent > 100) $progressPercent = 100;
@endphp
<div class="flex flex-col gap-1 max-w-[130px]">
<div class="flex justify-between text-[10px] font-extrabold">
<span class="text-slate-400">Doldurulan</span>
<span class="text-blue-600">{{ $filledDays }} / {{ $intern->internship_total_days }} Gün</span>
</div>
<div class="w-full bg-slate-100 h-2 rounded-full overflow-hidden border border-slate-200/40">
<div class="bg-blue-600 h-full rounded-full transition-all duration-300" style="width: {{ $progressPercent }}%"></div>
</div>
</div>
@else
<span class="text-slate-300 italic text-xs">Belirlenmemiş</span>
@endif
</td>
<!-- Status -->
<td class="py-4">
<span class="inline-flex px-2.5 py-1 rounded-full text-[10px] font-extrabold uppercase tracking-wider
@@ -61,22 +89,37 @@
<!-- Documents -->
<td class="py-4 space-y-1">
<div class="flex flex-wrap gap-1">
<div class="flex flex-wrap gap-1.5 max-w-[220px]">
@if($intern->certificate_code && ($intern->notebook_approved || $intern->status === 'accepted'))
<a href="{{ route('internship.verify', $intern->certificate_code) }}" target="_blank" class="btn-doc-emerald px-2 py-1 bg-emerald-600 hover:bg-emerald-700 text-white hover:text-white font-extrabold text-[10px] rounded-lg transition-all flex items-center gap-1 shadow-sm" title="Sertifika ve Akademik Transkript Doğrulama">
<i class="uil uil-award text-xs"></i> Sertifika & Transkript
</a>
@endif
@if($intern->internship_total_days)
<a href="{{ route('intern.print-journal') }}?size=a4&intern_id={{ $intern->id }}" target="_blank" class="btn-doc-blue px-2 py-1 bg-blue-50 hover:bg-blue-100 text-blue-700 hover:text-blue-800 font-bold text-[10px] rounded-lg transition-all flex items-center gap-1" title="A4 Staj Defterini Yazdır">
<i class="uil uil-print text-xs"></i> A4 Defter
</a>
<a href="{{ route('intern.print-journal') }}?size=a5&intern_id={{ $intern->id }}" target="_blank" class="btn-doc-slate px-2 py-1 bg-slate-100 hover:bg-slate-200 text-slate-700 hover:text-slate-900 font-bold text-[10px] rounded-lg transition-all flex items-center gap-1" title="A5 Staj Defterini Yazdır">
<i class="uil uil-print text-xs"></i> A5 Defter
</a>
@endif
@if($intern->cv_path)
<a href="{{ Storage::disk('public')->url($intern->cv_path) }}" target="_blank" class="px-2 py-1 bg-slate-100 hover:bg-slate-200 text-slate-600 font-bold text-[10px] rounded-lg transition-all flex items-center gap-1">
<i class="uil uil-file-alt"></i> CV
<a href="{{ Storage::disk('public')->url($intern->cv_path) }}" target="_blank" class="btn-doc-slate px-2 py-1 bg-slate-100 hover:bg-slate-200 text-slate-600 hover:text-slate-800 font-bold text-[10px] rounded-lg transition-all flex items-center gap-1" title="CV İndir">
<i class="uil uil-file-alt text-xs"></i> CV
</a>
@endif
@if($intern->to_be_signed_internship_form_path)
<a href="{{ Storage::disk('public')->url($intern->to_be_signed_internship_form_path) }}" target="_blank" class="px-2 py-1 bg-blue-50 hover:bg-blue-100 text-blue-600 font-bold text-[10px] rounded-lg transition-all flex items-center gap-1">
<i class="uil uil-file-upload"></i> İmzalanacak Form
<a href="{{ Storage::disk('public')->url($intern->to_be_signed_internship_form_path) }}" target="_blank" class="btn-doc-amber px-2 py-1 bg-amber-50 hover:bg-amber-100 text-amber-700 hover:text-amber-800 font-bold text-[10px] rounded-lg transition-all flex items-center gap-1" title="Stajyerin Yüklediği İmzalanacak Form">
<i class="uil uil-file-upload text-xs"></i> Başvuru Formu
</a>
@endif
@if($intern->signed_internship_form_path)
<a href="{{ Storage::disk('public')->url($intern->signed_internship_form_path) }}" target="_blank" class="px-2 py-1 bg-green-50 hover:bg-green-100 text-green-600 font-bold text-[10px] rounded-lg transition-all flex items-center gap-1">
<i class="uil uil-file-check-alt"></i> İmzalı Form
<a href="{{ Storage::disk('public')->url($intern->signed_internship_form_path) }}" target="_blank" class="btn-doc-green px-2 py-1 bg-green-50 hover:bg-green-100 text-green-700 hover:text-green-800 font-bold text-[10px] rounded-lg transition-all flex items-center gap-1" title="Kurum Tarafından İmzalanmış Staj Formu">
<i class="uil uil-file-check-alt text-xs"></i> İmzalı Form
</a>
@endif
</div>
@@ -86,11 +129,11 @@
<td class="py-4">
@if($intern->github_repo)
<div class="flex flex-col gap-1">
<a href="{{ $intern->github_repo }}" target="_blank" class="text-blue-600 hover:text-blue-800 font-extrabold text-xs inline-flex items-center gap-1">
<a href="{{ $intern->github_repo }}" target="_blank" class="btn-doc-repo text-blue-600 hover:text-blue-800 font-extrabold text-xs inline-flex items-center gap-1">
<i class="uil uil-github"></i> Depoyu Aç
</a>
<!-- If they have repo, we can fetch their journal commits or trigger the controller download markdown -->
<a href="{{ route('intern.download-journal') }}?intern_id={{ $intern->id }}" class="px-2 py-1 bg-emerald-50 hover:bg-emerald-100 text-emerald-600 font-bold text-[10px] rounded-lg transition-all flex items-center justify-center gap-1 w-max">
<a href="{{ route('intern.download-journal') }}?intern_id={{ $intern->id }}" class="btn-doc-md px-2 py-1 bg-emerald-50 hover:bg-emerald-100 text-emerald-600 hover:text-emerald-700 font-bold text-[10px] rounded-lg transition-all flex items-center justify-center gap-1 w-max">
<i class="uil uil-arrow-down-tray"></i> Günlüğü İndir (.md)
</a>
</div>
@@ -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 => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&#39;',
'"': '&quot;'
}[tag] || tag)
);
}
// Handle ESC key for quick approval modal
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeQuickEntryModal();
}
});
</script>
+20 -23
View File
@@ -207,11 +207,25 @@
background: #ffffff;
margin-bottom: 20px;
min-height: 250px;
white-space: pre-wrap;
line-height: 1.6;
color: #334155;
font-size: 13px;
}
.content-area ul {
list-style-type: disc !important;
padding-left: 20px !important;
margin-top: 8px !important;
margin-bottom: 8px !important;
}
.content-area ol {
list-style-type: decimal !important;
padding-left: 20px !important;
margin-top: 8px !important;
margin-bottom: 8px !important;
}
.content-area li {
margin-bottom: 4px !important;
}
.empty-content {
color: #94a3b8;
@@ -317,8 +331,8 @@
<span>Panele Dön</span>
</a>
<div style="border-left: 1px solid #e2e8f0; margin: 0 5px; height: 35px;"></div>
<a href="?size=a4{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}" class="btn {{ $size === 'a4' ? 'btn-primary' : 'btn-secondary' }}">A4 Boyutu</a>
<a href="?size=a5{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}" class="btn {{ $size === 'a5' ? 'btn-primary' : 'btn-secondary' }}">A5 Boyutu</a>
<a href="?size=a4{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}{{ request()->has('day') ? '&day='.request('day') : '' }}" class="btn {{ $size === 'a4' ? 'btn-primary' : 'btn-secondary' }}">A4 Boyutu</a>
<a href="?size=a5{{ request()->has('intern_id') ? '&intern_id='.request('intern_id') : '' }}{{ request()->has('day') ? '&day='.request('day') : '' }}" class="btn {{ $size === 'a5' ? 'btn-primary' : 'btn-secondary' }}">A5 Boyutu</a>
<button onclick="window.print()" class="btn btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
<span>Yazdır / PDF Kaydet</span>
@@ -383,7 +397,7 @@
<!-- Content Area -->
<div class="content-area">
@if($entry && trim($entry->content))
{!! nl2br(e($entry->content)) !!}
{!! $entry->content !!}
@else
<div class="empty-content">Bu gün için herhangi bir staj raporu girilmemiştir.</div>
@endif
@@ -406,29 +420,12 @@
<!-- Supervisor -->
<div class="signature-box">
<div class="sig-title">Staj Sorumlusu</div>
@if($intern->notebook_supervisor_signed)
@if($entry && $entry->supervisor_approved)
<div class="sig-status">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color: #059669;"><polyline points="20 6 9 17 4 12"/></svg>
<span class="sig-badge">E-ONAYLI</span>
</div>
<div class="sig-name">{{ $intern->notebook_supervisor_name ?: 'Alperen Trunç' }}</div>
@else
<div class="sig-status waiting">
<span class="sig-badge waiting">İMZA BEKLENİYOR</span>
</div>
<div class="sig-name">&nbsp;</div>
@endif
</div>
<!-- Unit Officer -->
<div class="signature-box">
<div class="sig-title">Birim Yetkilisi</div>
@if($intern->notebook_unit_signed)
<div class="sig-status">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color: #059669;"><polyline points="20 6 9 17 4 12"/></svg>
<span class="sig-badge">E-ONAYLI</span>
</div>
<div class="sig-name">{{ $intern->notebook_unit_name ?: 'Yetkili Birim Amiri' }}</div>
<div class="sig-name">{{ $entry->supervisor_name ?: ($intern->notebook_supervisor_name ?: 'Alperen Trunç') }}</div>
@else
<div class="sig-status waiting">
<span class="sig-badge waiting">İMZA BEKLENİYOR</span>
+119 -81
View File
@@ -3,6 +3,7 @@
@section('content')
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700;800;900&family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,500;0,700;1,400&display=swap" rel="stylesheet">
@if($application->status === 'accepted' && ($application->notebook_approved || $application->notebook_supervisor_signed || !empty($application->transcript_markdown)))
<div class="bg-slate-100 py-12 no-print">
<div class="container max-w-6xl mx-auto px-4">
<!-- Verification Banner (e-Devlet Style) -->
@@ -16,93 +17,21 @@
<span class="text-slate-600 ml-1">Bu staj sertifikası ve transkripti, Trunçgil Teknopark sistemi üzerinden dijital olarak imzalanıp onaylanmıştır.</span>
</div>
</div>
<div class="flex-shrink-0">
<div class="flex-shrink-0 flex items-center gap-2">
<!-- Print Journal Buttons -->
<a href="{{ route('intern.print-journal') }}?size=a4&intern_id={{ $application->id }}" target="_blank" class="px-3.5 py-2 bg-white hover:bg-slate-50 text-slate-700 border border-slate-200 text-xs font-bold rounded-lg transition-all shadow-sm flex items-center gap-1.5 whitespace-nowrap">
<i class="uil uil-print text-sm"></i>
<span>A4 Defter</span>
</a>
<!-- PDF Download Button -->
<button onclick="downloadPDF()" id="pdf-btn" class="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white text-sm font-bold rounded-lg transition-all shadow-sm flex items-center gap-1.5 whitespace-nowrap">
<button onclick="downloadPDF()" id="pdf-btn" class="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white text-sm font-bold rounded-lg transition-all shadow-sm flex items-center gap-1.5 whitespace-nowrap cursor-pointer">
<i class="uil uil-file-download text-base" id="pdf-icon"></i>
<span class="spinner-border spinner-border-sm hidden animate-spin w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full" id="pdf-spinner" role="status"></span>
<span id="pdf-btn-text">PDF İndir</span>
<span id="pdf-btn-text">PDF Olarak İndir</span>
</button>
</div>
</div>
<!-- ================= PAGE 3: STAJ DEFTERİ (PORTRAIT A4) ================= -->
@if($application->notebook_approved || $application->notebook_supervisor_signed || $application->notebook_unit_signed)
<div class="notebook-container-wrapper overflow-hidden w-full flex justify-center items-start mt-8">
<div id="notebook-node" class="transcript-page bg-white rounded-[2rem] shadow-2xl p-6 md:p-8 relative overflow-hidden mx-auto">
<!-- Thin border decor for notebook -->
<div class="absolute inset-4 border border-slate-100 rounded-[1.5rem] pointer-events-none"></div>
<div class="relative z-10 flex flex-col justify-between h-full text-slate-700">
<div>
<!-- Header -->
<div class="flex justify-between items-center pb-2 border-b border-slate-100 mb-3">
<div>
<h2 class="font-serif font-bold text-slate-900 transcript-header-title">ONAYLI STAJ DEFTERİ RAPORU</h2>
<p class="text-slate-400 tracking-wider mt-0.5 transcript-header-subtitle">DİJİTAL İMZALI VE ONAYLI STAJ RAPORLARI</p>
</div>
<img src="{{ asset('logos/truncgil-yatay.svg') }}" alt="Trunçgil Logo" class="h-6 object-contain">
</div>
<!-- Signatures Information -->
<div class="bg-emerald-50 border border-emerald-200 rounded-xl p-3 mb-4 flex justify-between items-center">
<div>
<span class="text-xs font-extrabold text-emerald-800 uppercase block tracking-wider">Dijital İmza Durumu</span>
<span class="text-[10px] text-emerald-600 font-semibold block mt-0.5">Bu staj defteri yetkililer tarafından elektronik olarak imzalanmıştır.</span>
</div>
<div class="flex gap-2">
@if($application->notebook_supervisor_signed)
<span class="inline-flex px-2 py-1 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">
Sorumlu: {{ $application->notebook_supervisor_name ?: 'Alperen Trunç' }}
</span>
@endif
@if($application->notebook_unit_signed)
<span class="inline-flex px-2 py-1 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">
Yetkili: {{ $application->notebook_unit_name ?: 'Birim Yetkilisi' }}
</span>
@endif
</div>
</div>
<!-- Notebook Days content overview -->
<div class="overflow-y-auto max-h-[700px] pr-2 no-scrollbar space-y-4">
@foreach($days as $day)
@php
$entry = $savedEntries->get($day['day_number']);
@endphp
@if($entry && trim($entry->content))
<div class="p-3 bg-slate-50 border border-slate-100 rounded-xl">
<div class="flex justify-between items-center pb-1.5 border-b border-slate-200/50 mb-1.5">
<span class="text-xs font-extrabold text-slate-800">{{ $day['day_number'] }}. Gün Raporu</span>
<span class="text-[10px] text-slate-400 font-bold">{{ $day['formatted_date'] }}</span>
</div>
<p class="text-xs text-slate-600 whitespace-pre-wrap leading-relaxed" style="font-size: 11px;">{{ $entry->content }}</p>
</div>
@endif
@endforeach
</div>
</div>
<!-- Footer -->
<div class="w-full flex justify-between items-center pt-2 border-t border-slate-100 gap-4 mt-auto">
<div class="flex items-center gap-3">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data={{ urlencode(route('internship.verify', $application->certificate_code)) }}" alt="QR Code" class="w-14 h-14 border border-slate-200 p-0.5 rounded-lg bg-white">
<div>
<span class="font-bold text-slate-400 tracking-wider block verify-code-label">GÜVENLİ DOĞRULAMA KODU</span>
<span class="font-mono font-bold text-slate-800 block verify-code">{{ $application->certificate_code }}</span>
</div>
</div>
<div class="text-right text-slate-400 footer-address">
Trunçgil Teknoloji San. ve Tic. Ltd. Şti.<br>
Gaziantep Üniversitesi Teknopark No: 4A
</div>
</div>
</div>
</div>
</div>
@endif
</div>
</div>
@@ -146,7 +75,7 @@
<p>
<strong>{{ \Carbon\Carbon::parse($application->internship_start_date)->format('d.m.Y') }}</strong> -
<strong>{{ \Carbon\Carbon::parse($application->internship_end_date)->format('d.m.Y') }}</strong> tarihleri arasında,
<strong>Trunçgil Teknoloji</strong> bünyesinde gerçekleştirdiği uzaktan staj programını başarıyla tamamlayarak bu belgeyi almaya hak kazanmıştır.
<strong>Trunçgil Teknoloji</strong> bünyesinde gerçekleştirdiği staj programını başarıyla tamamlayarak bu belgeyi almaya hak kazanmıştır.
</p>
<p class="text-xs md:text-sm text-slate-500 max-w-2xl mx-auto italic font-serif">
Gösterdiği üstün gayret, sorumluluk bilinci, teknik beceriler ve ekip çalışmasına sağladığı değerli katkılardan dolayı teşekkür eder, profesyonel kariyerinde başarılar dileriz.
@@ -242,18 +171,127 @@
</div>
</div>
<!-- ================= PAGE 3: STAJ DEFTERİ (PORTRAIT A4) ================= -->
@if($application->notebook_approved || $application->notebook_supervisor_signed)
<div class="notebook-container-wrapper overflow-hidden w-full flex justify-center items-start mt-8">
<div id="notebook-node" class="transcript-page bg-white rounded-[2rem] shadow-2xl p-6 md:p-8 relative overflow-hidden mx-auto">
<!-- Thin border decor for notebook -->
<div class="absolute inset-4 border border-slate-100 rounded-[1.5rem] pointer-events-none"></div>
<div class="relative z-10 flex flex-col justify-between h-full text-slate-700">
<div>
<!-- Header -->
<div class="flex justify-between items-center pb-2 border-b border-slate-100 mb-3">
<div>
<h2 class="font-serif font-bold text-slate-900 transcript-header-title">ONAYLI STAJ DEFTERİ RAPORU</h2>
<p class="text-slate-400 tracking-wider mt-0.5 transcript-header-subtitle">DİJİTAL İMZALI VE ONAYLI STAJ RAPORLARI</p>
</div>
<img src="{{ asset('logos/truncgil-yatay.svg') }}" alt="Trunçgil Logo" class="h-6 object-contain">
</div>
<!-- Signatures Information -->
<div class="bg-emerald-50 border border-emerald-200 rounded-xl p-3 mb-4 flex justify-between items-center">
<div>
<span class="text-xs font-extrabold text-emerald-800 uppercase block tracking-wider">Dijital İmza Durumu</span>
<span class="text-[10px] text-emerald-600 font-semibold block mt-0.5">Bu staj defteri yetkililer tarafından elektronik olarak imzalanmıştır.</span>
</div>
<div class="flex gap-2">
@if($application->notebook_supervisor_signed)
<span class="inline-flex px-2 py-1 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">
Sorumlu: {{ $application->notebook_supervisor_name ?: 'Alperen Trunç' }}
</span>
@endif
</div>
</div>
<!-- Notebook Days content overview -->
<div class="overflow-y-auto max-h-[700px] pr-2 no-scrollbar space-y-4">
@foreach($days as $day)
@php
$entry = $savedEntries->get($day['day_number']);
@endphp
@if($entry && trim($entry->content))
<div class="p-3 bg-slate-50 border border-slate-100 rounded-xl">
<div class="flex justify-between items-center pb-1.5 border-b border-slate-200/50 mb-1.5">
<span class="text-xs font-extrabold text-slate-800">
{{ $day['day_number'] }}. Gün Raporu
@if($entry->supervisor_approved)
<span class="ml-2 inline-flex px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-800 text-[8px] font-extrabold uppercase border border-emerald-200">Sorumlu Onaylı{{ $entry->supervisor_name ? ' (' . $entry->supervisor_name . ')' : '' }}</span>
@endif
</span>
<span class="text-[10px] text-slate-400 font-bold">{{ $day['formatted_date'] }}</span>
</div>
<div class="text-xs text-slate-600 leading-relaxed verify-log-content" style="font-size: 11px;">{!! $entry->content !!}</div>
</div>
@endif
@endforeach
</div>
</div>
<!-- Footer -->
<div class="w-full flex justify-between items-center pt-2 border-t border-slate-100 gap-4 mt-auto">
<div class="flex items-center gap-3">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data={{ urlencode(route('internship.verify', $application->certificate_code)) }}" alt="QR Code" class="w-14 h-14 border border-slate-200 p-0.5 rounded-lg bg-white">
<div>
<span class="font-bold text-slate-400 tracking-wider block verify-code-label">GÜVENLİ DOĞRULAMA KODU</span>
<span class="font-mono font-bold text-slate-800 block verify-code">{{ $application->certificate_code }}</span>
</div>
</div>
<div class="text-right text-slate-400 footer-address">
Trunçgil Teknoloji San. ve Tic. Ltd. Şti.<br>
Gaziantep Üniversitesi Teknopark No: 4A
</div>
</div>
</div>
</div>
</div>
@endif
</div>
</div>
@else
<div class="min-h-screen bg-slate-100 flex items-center justify-center p-6 no-print">
<div class="bg-white rounded-3xl p-8 max-w-md w-full shadow-2xl border border-slate-100 text-center">
<div class="w-16 h-16 rounded-full bg-red-50 text-[#e31e24] flex items-center justify-center text-3xl mx-auto mb-4">
<i class="uil uil-exclamation-triangle"></i>
</div>
<h1 class="text-xl font-bold text-slate-800">Doğrulanamayan Belge</h1>
<p class="text-xs text-slate-500 mt-2 leading-relaxed">Görüntülemeye çalıştığınız staj kaydı henüz onaylanmamıştır veya aktif değildir. Detaylı bilgi için kurum yetkilileri ile iletişime geçebilirsiniz.</p>
<a href="/" class="mt-6 inline-flex px-5 py-2.5 bg-slate-800 hover:bg-slate-900 text-white font-bold rounded-xl text-xs transition-all shadow-md">Anasayfaya Dön</a>
</div>
</div>
@endif
@push('styles')
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
/* Prevent red text on button hover */
.no-print a:hover, .no-print a:focus {
color: inherit !important;
text-decoration: none !important;
}
:root {
/* Global scale factor for transcript page fonts. Adjust this easily (e.g. 1.0, 1.10, 1.20) */
--transcript-font-scale: 1.30;
}
.verify-log-content ul {
list-style-type: disc !important;
padding-left: 18px !important;
margin-top: 5px !important;
margin-bottom: 5px !important;
}
.verify-log-content ol {
list-style-type: decimal !important;
padding-left: 18px !important;
margin-top: 5px !important;
margin-bottom: 5px !important;
}
.verify-log-content li {
margin-bottom: 3px !important;
}
body {
font-family: 'Inter', sans-serif;
+20 -1
View File
@@ -17,16 +17,35 @@ $favicon_path = setting('site_favicon');
}
@endphp
@php
$seo_canonical = $meta['canonical'] ?? url()->current();
$seo_canonical = $meta['canonical'] ?? null;
if (!$seo_canonical) {
$seo_canonical = url()->current();
$localeQuery = request()->query('locale') ?? request()->query('lang');
if ($localeQuery && function_exists('available_language_codes') && in_array($localeQuery, available_language_codes())) {
$seo_canonical .= '?locale=' . $localeQuery;
}
}
$seo_robots = $meta['robots'] ?? 'index, follow';
$seo_og_type = $meta['og_type'] ?? 'website';
$seo_og_locale = str_replace('_', '-', $meta['locale'] ?? app()->getLocale());
$activeLanguages = class_exists(\App\Models\Language::class)
? \App\Models\Language::where('is_active', true)->get()
: collect([]);
$currentUrl = url()->current();
@endphp
<title>{{ $seo_title }}</title>
<meta name="description" content="{{ $seo_description }}">
<meta name="robots" content="{{ $seo_robots }}">
<link rel="canonical" href="{{ $seo_canonical }}">
@foreach($activeLanguages as $lang)
<link rel="alternate" hreflang="{{ $lang->code }}" href="{{ $currentUrl . '?locale=' . $lang->code }}">
@endforeach
@if($activeLanguages->count() > 0)
<link rel="alternate" hreflang="x-default" href="{{ $currentUrl }}">
@endif
<!-- Open Graph / Facebook -->
<meta property="og:type" content="{{ $seo_og_type }}">
<meta property="og:url" content="{{ $seo_canonical }}">
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+198 -143
View File
@@ -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) -->
@@ -73,65 +74,55 @@
box-shadow: 0 0 8px var(--accent-color-glow);
}
/* Rendered Markdown Styling */
.proposal-content h1 {
font-family: 'Outfit', sans-serif;
font-size: 2.25rem;
font-weight: 800;
color: #0f172a;
margin-top: 2rem;
margin-bottom: 1rem;
line-height: 1.2;
}
.dark .proposal-content h1 { color: #f8fafc; }
.proposal-content h2 {
font-family: 'Outfit', sans-serif;
font-size: 1.6rem;
font-weight: 700;
color: #1e293b;
margin-top: 2.5rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
line-height: 1.3;
}
.dark .proposal-content h2 {
color: #f1f5f9;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
/* Rendered Markdown Styling & Responsive Overflow Control */
.proposal-content {
word-break: break-word;
overflow-wrap: anywhere;
}
.proposal-content h3 {
font-family: 'Outfit', sans-serif;
font-size: 1.25rem;
font-weight: 600;
color: #334155;
margin-top: 1.8rem;
margin-bottom: 0.75rem;
.proposal-content img {
max-width: 100%;
height: auto;
border-radius: 12px;
}
.dark .proposal-content h3 { color: #e2e8f0; }
.proposal-content p {
font-size: 1rem;
line-height: 1.7;
color: #475569;
margin-bottom: 1.25rem;
.proposal-content pre {
max-width: 100%;
overflow-x: auto;
border-radius: 12px;
white-space: pre-wrap;
word-break: break-word;
}
.proposal-content code {
word-break: break-word;
}
/* Responsive Table Wrapper */
.table-responsive {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
margin: 1.5rem 0;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.03);
}
.dark .table-responsive {
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: none;
}
.dark .proposal-content p { color: #94a3b8; }
/* Tables inside Markdown styling */
.proposal-content table {
width: 100%;
margin: 2rem 0;
min-width: 600px;
margin: 0;
border-collapse: collapse;
font-size: 0.925rem;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
font-size: 0.9rem;
border: none;
}
.dark .proposal-content table {
border: 1px solid rgba(255, 255, 255, 0.05);
border: none;
box-shadow: none;
}
@@ -296,7 +287,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 +463,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',
@@ -644,8 +696,8 @@
</article>
<!-- Interactive pricing & Gaziantep Teknopark %0 KDV muafiyeti box -->
@if($proposal->total_price && ($proposal->meta['show_calculator'] ?? true))
<!-- Fixed Gaziantep Teknopark %0 KDV muafiyeti & Net Fiyat box -->
@if($proposal->total_price)
<section class="p-6 sm:p-8 rounded-2xl bg-white dark:bg-slate-800 border border-slate-200/60 dark:border-slate-700/50 shadow-sm relative overflow-hidden transition-all duration-300">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r {{ $gradient }}"></div>
@@ -657,39 +709,29 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.745 3.745 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043A3.746 3.746 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z" />
</svg>
<span>Teknopark KDV Muafiyeti Avantajı</span>
<span>Teknopark KDV Muafiyeti (%0 KDV)</span>
</div>
<h3 class="text-xl font-display font-bold text-slate-900 dark:text-white mb-2">
4691 Sayılı Kanun KDV Muafiyeti
4691 Sayılı Kanun KDV Muafiyetli Net Fiyat
</h3>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed mb-0">
Trunçgil Teknoloji, Gaziantep Üniversitesi Teknoparkı bünyesinde AR-GE yürüten tescilli bir kuruluştur. Kanun kapsamında geliştirdiğimiz yazılım çözümleri KDV'den (%0 KDV) istisnadır. Bu sayede maliyetlerinizde doğrudan avantaj sağlarsınız.
Trunçgil Teknoloji, Gaziantep Üniversitesi Teknoparkı bünyesinde AR-GE yürüten tescilli bir kuruluştur. 4691 Sayılı Kanun kapsamında geliştirdiğimiz yazılım çözümleri KDV'den (%0 KDV) istisnadır.
</p>
</div>
<!-- Interactive Box with Toggle Switch -->
<!-- Price Summary Box (Fixed Net Price, Non-Optional) -->
<div class="w-full md:w-80 shrink-0 p-5 rounded-2xl bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 flex flex-col gap-4">
<div class="flex items-center justify-between">
<span class="text-xs text-slate-400 dark:text-slate-500 font-bold uppercase">KDV Muafiyeti Göster</span>
<!-- Toggle Switch -->
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="kdv-toggle" class="sr-only peer" checked>
<div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-slate-600 peer-checked:bg-emerald-500"></div>
</label>
</div>
<div class="border-t border-slate-200/50 dark:border-slate-800/80 pt-3">
<div class="text-xs text-slate-400 dark:text-slate-500 font-semibold mb-1" id="kdv-label">Teknopark Avantajlı Fiyat (%0 KDV)</div>
<div class="text-2xl font-display font-extrabold text-slate-900 dark:text-white" id="kdv-price">
<div>
<div class="text-xs text-slate-400 dark:text-slate-500 font-semibold mb-1">Teknopark Muafiyetli Net Fiyat (%0 KDV)</div>
<div class="text-2xl font-display font-extrabold text-slate-900 dark:text-white">
{{ number_format($proposal->total_price, 2, ',', '.') }} {{ $proposal->currency === 'USD' ? 'USD' : ($proposal->currency === 'EUR' ? 'EUR' : 'TL') }}
</div>
</div>
<div class="p-2.5 rounded-lg bg-emerald-50 dark:bg-emerald-950/20 text-emerald-800 dark:text-emerald-400 border border-emerald-200/20 text-xs font-semibold leading-relaxed flex items-start gap-2" id="kdv-benefit-box">
<div class="p-2.5 rounded-lg bg-emerald-50 dark:bg-emerald-950/20 text-emerald-800 dark:text-emerald-400 border border-emerald-200/20 text-xs font-semibold leading-relaxed flex items-start gap-2">
<i data-lucide="sparkles" class="w-4 h-4 shrink-0 text-emerald-500"></i>
<span>
%20 standart KDV oranına kıyasla toplamda <strong id="kdv-benefit-amount">{{ number_format($proposal->total_price * 0.20, 2, ',', '.') }}</strong> kazanç sağladınız.
%20 standart KDV oranına kıyasla toplamda <strong>{{ number_format($proposal->total_price * 0.20, 2, ',', '.') }} {{ $proposal->currency === 'USD' ? 'USD' : ($proposal->currency === 'EUR' ? 'EUR' : 'TL') }}</strong> net KDV tasarrufu sağlandı.
</span>
</div>
</div>
@@ -962,14 +1004,53 @@
const preprocessedText = parseGithubAlerts(rawMarkdown);
contentDiv.innerHTML = marked.parse(preprocessedText);
// Auto-wrap markdown tables in responsive overflow wrapper to prevent design overflow
contentDiv.querySelectorAll('table').forEach(table => {
if (!table.parentElement.classList.contains('table-responsive')) {
const wrapper = document.createElement('div');
wrapper.className = 'table-responsive';
table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table);
}
});
// Render Mermaid Diagrams
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 +1067,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 +1120,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 +1143,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,20 +1161,22 @@
<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>
<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>
<button class="mermaid-zoom-btn" title="1:1 Sıfırla" onclick="mermaidFsZoom('reset')">
<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 +1192,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 +1208,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 +1272,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);
@@ -1406,31 +1485,7 @@
});
});
// 4. KDV Switch Calculator Logic
const kdvToggle = document.getElementById('kdv-toggle');
const kdvLabel = document.getElementById('kdv-label');
const kdvPrice = document.getElementById('kdv-price');
const kdvBenefitBox = document.getElementById('kdv-benefit-box');
if (kdvToggle) {
const totalPrice = parseFloat('{{ $proposal->total_price }}');
const currencySymbol = '{{ $proposal->currency === 'USD' ? 'USD' : ($proposal->currency === 'EUR' ? 'EUR' : 'TL') }}';
kdvToggle.addEventListener('change', function() {
if (this.checked) {
// %0 KDV selected (Teknopark exemption)
kdvLabel.textContent = 'Teknopark Avantajlı Fiyat (%0 KDV)';
kdvPrice.textContent = new Intl.NumberFormat('tr-TR', { minimumFractionDigits: 2 }).format(totalPrice) + ' ' + currencySymbol;
kdvBenefitBox.classList.remove('hidden');
} else {
// %20 standard KDV added
const priceWithVat = totalPrice * 1.20;
kdvLabel.textContent = 'Standart Fiyat (+%20 KDV)';
kdvPrice.textContent = new Intl.NumberFormat('tr-TR', { minimumFractionDigits: 2 }).format(priceWithVat) + ' ' + currencySymbol;
kdvBenefitBox.classList.add('hidden');
}
});
}
// 4. Client Interaction Actions & Tabs switching
// 5. Client Interaction Actions & Tabs switching
function switchTab(tab) {
+7 -1
View File
@@ -1,8 +1,14 @@
{!! '<' . '?xml version="1.0" encoding="UTF-8"?' . '>' !!}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
@foreach($urls as $url)
<url>
<loc>{{ $url['loc'] }}</loc>
@if(!empty($url['alternates']))
@foreach($url['alternates'] as $alt)
<xhtml:link rel="alternate" hreflang="{{ $alt['hreflang'] }}" href="{{ $alt['href'] }}"/>
@endforeach
@endif
<lastmod>{{ $url['lastmod'] }}</lastmod>
<changefreq>{{ $url['changefreq'] }}</changefreq>
<priority>{{ $url['priority'] }}</priority>
+37
View File
@@ -102,6 +102,12 @@ Route::prefix('admin/api')->middleware(['auth', \App\Http\Middleware\SuperAdminM
Route::post('/site-translations/batch-destroy', [SiteTranslationController::class, 'batchDestroy'])->name('api.site-translations.batch-destroy');
});
// LinkedIn OAuth Routes (Admin Yetkilendirme)
Route::middleware(['auth'])->prefix('admin/linkedin')->group(function () {
Route::get('/connect', [\App\Http\Controllers\Admin\LinkedInController::class, 'connect'])->name('admin.linkedin.connect');
Route::get('/callback', [\App\Http\Controllers\Admin\LinkedInController::class, 'callback'])->name('admin.linkedin.callback');
});
// Products & Services
Route::redirect('/urun-hizmet/Yazılım Danışmanlık', '/urun-hizmet/yazilim-danismanlik', 301);
Route::get('/urun-hizmet/{slug}', [\App\Http\Controllers\ProductController::class, 'show'])->name('products.show');
@@ -124,6 +130,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');
@@ -131,6 +139,11 @@ Route::post('/stajyer/cikis', [\App\Http\Controllers\CareerController::class, 'i
Route::get('/stajyer/admin/giris', [\App\Http\Controllers\CareerController::class, 'internAdminLoginForm'])->name('intern.admin.login');
Route::post('/stajyer/admin/giris', [\App\Http\Controllers\CareerController::class, 'internAdminLogin'])->name('intern.admin.login.submit');
Route::get('/stajyer/admin/panel', [\App\Http\Controllers\CareerController::class, 'internAdminDashboard'])->name('intern.admin.dashboard');
Route::get('/stajyer/admin/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');
@@ -167,6 +180,30 @@ 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');
// Trunçgil OEM B2B - Hidden Test Application
Route::get('/truncgil-oem-b2b', function () {
return view('oem_b2b_demo');
})->name('oem.b2b.demo');
Route::get('/oem-b2b-demo', function () {
return view('oem_b2b_demo');
});
// Live OEM & Barcode Internet Lookup API
Route::get('/api/oem-lookup', [\App\Http\Controllers\OemLookupController::class, 'lookup'])->name('oem.lookup');
// Privacy Policy Alternatives
Route::get('/privacy-policy', function () {
return app(PageController::class)->show('privacy');
+306
View File
@@ -0,0 +1,306 @@
[
{
"id": 18,
"name": "Emre Satıl",
"email": "emresatil72@gmail.com",
"status": "accepted",
"start_date": "2026-07-01",
"end_date": "2026-08-30",
"total_days": 43,
"filled_days": 42,
"total_entries": 42,
"github_repo": "https:\/\/github.com\/Emresatil\/Recycle-Rush-VR\/commits\/release\/v1.0-final-integration-and-bugs",
"github_username": "Emresatil",
"certificate_code": "TRN-2026-0QQJ-DAC6",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 19,
"name": "Ayşenur Ebrar Gündüz",
"email": "aysenurebrargunduzz@gmail.com",
"status": "accepted",
"start_date": "2026-07-20",
"end_date": "2026-08-14",
"total_days": 20,
"filled_days": 20,
"total_entries": 20,
"github_repo": "https:\/\/github.com\/AysenurGunduz\/vantage-ai",
"github_username": "AysenurGunduz",
"certificate_code": "TRN-2026-KB58-U0RO",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 20,
"name": "Hakan Üzer",
"email": "hakanuzer1@gmail.com",
"status": "accepted",
"start_date": "2026-07-01",
"end_date": "2026-08-30",
"total_days": 43,
"filled_days": 42,
"total_entries": 42,
"github_repo": "https:\/\/github.com\/Emresatil\/Recycle-Rush-VR\/tree\/release\/final-integration-and-bugs",
"github_username": "Hakan460",
"certificate_code": "TRN-2026-5W3W-I6HQ",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 22,
"name": "Mustafa emre kaplan",
"email": "mustafaemre027@gmail.com",
"status": "accepted",
"start_date": "2026-07-13",
"end_date": "2026-08-10",
"total_days": 21,
"filled_days": 19,
"total_entries": 19,
"github_repo": "https:\/\/github.com\/mustafaemre027\/securewatch-ai",
"github_username": "mustafaemre027",
"certificate_code": "TRN-2026-ZYEP-XV8B",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 23,
"name": "Doğukan Kalkan",
"email": "kalkandogukan01@gmail.com",
"status": "accepted",
"start_date": "2026-06-29",
"end_date": "2026-07-24",
"total_days": 20,
"filled_days": 20,
"total_entries": 20,
"github_repo": "https:\/\/github.com\/Dogukan-klkn\/StockRoute",
"github_username": "Dogukan-klkn",
"certificate_code": "TRN-2026-WIVJ-1UD4",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 24,
"name": "Elif Çiftepala",
"email": "elifciftepala82@gmail.com",
"status": "rejected",
"start_date": null,
"end_date": null,
"total_days": null,
"filled_days": 0,
"total_entries": 0,
"github_repo": null,
"github_username": null,
"certificate_code": "TRN-2026-XWNU-9BUD",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 25,
"name": "Eren Kara",
"email": "erenkara1549@gmail.com",
"status": "accepted",
"start_date": "2026-07-06",
"end_date": "2026-08-15",
"total_days": 30,
"filled_days": 30,
"total_entries": 30,
"github_repo": "https:\/\/github.com\/erenkara0\/Smart-E-Commerce-Assistant",
"github_username": "erenkara0",
"certificate_code": "TRN-2026-YJST-MLYF",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 26,
"name": "Ümit Tunç",
"email": "umit.tunc@truncgil.com",
"status": "rejected",
"start_date": "2026-05-01",
"end_date": "2026-05-28",
"total_days": 20,
"filled_days": 0,
"total_entries": 0,
"github_repo": null,
"github_username": null,
"certificate_code": "TRN-2026-D3SB-RVTU",
"has_custom_transcript": true,
"transcript_length": 2635,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 27,
"name": "ismet can sezgin",
"email": "ismet.can.sezgin96@erzurum.edu.tr",
"status": "accepted",
"start_date": "2026-07-13",
"end_date": "2026-08-07",
"total_days": 20,
"filled_days": 20,
"total_entries": 20,
"github_repo": "https:\/\/github.com\/ismetcansezgin\/EEG-Flow",
"github_username": "ismetcansezgin",
"certificate_code": "TRN-2026-SU3A-6IIZ",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 28,
"name": "Alesam Baath",
"email": "isambais15@gmail.com",
"status": "accepted",
"start_date": "2026-07-13",
"end_date": "2026-08-07",
"total_days": 20,
"filled_days": 19,
"total_entries": 19,
"github_repo": "https:\/\/github.com\/isambais\/SmartHome-EnergyRL",
"github_username": "isambais",
"certificate_code": "TRN-2026-06W3-S9FX",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 29,
"name": "Mehmet Akif Tunçer",
"email": "akiftuncer0@gmail.com",
"status": "rejected",
"start_date": null,
"end_date": null,
"total_days": null,
"filled_days": 0,
"total_entries": 0,
"github_repo": null,
"github_username": null,
"certificate_code": "TRN-2026-IXC3-PWKV",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 30,
"name": "Melike Bayer",
"email": "melikebayer09@gmail.com",
"status": "accepted",
"start_date": "2026-08-14",
"end_date": "2026-09-10",
"total_days": 20,
"filled_days": 0,
"total_entries": 0,
"github_repo": null,
"github_username": null,
"certificate_code": "TRN-2026-QMJN-R5AS",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 31,
"name": "Faruk Tazeoğlu",
"email": "faruktazeoglu9@gmail.com",
"status": "accepted",
"start_date": "2026-07-10",
"end_date": "2026-08-10",
"total_days": 22,
"filled_days": 22,
"total_entries": 22,
"github_repo": "https:\/\/github.com\/Faruk-T\/baret",
"github_username": "Faruk-T",
"certificate_code": "TRN-2026-2EM8-AL2H",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 32,
"name": "Barış Paşa",
"email": "barispasa460@gmail.com",
"status": "accepted",
"start_date": "2026-07-23",
"end_date": "2026-08-23",
"total_days": 22,
"filled_days": 16,
"total_entries": 16,
"github_repo": "https:\/\/github.com\/baris8138\/UstaFlow_litte",
"github_username": "baris8138",
"certificate_code": "TRN-2026-I7Y0-EDD1",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 33,
"name": "Sena Karabikci",
"email": "karabikcisena@gmail.com",
"status": "pending",
"start_date": null,
"end_date": null,
"total_days": null,
"filled_days": 0,
"total_entries": 0,
"github_repo": null,
"github_username": null,
"certificate_code": "TRN-2026-XSUY-LFMV",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
},
{
"id": 37,
"name": "ibrahim şaar",
"email": "sharabrahym56@gmail.com",
"status": "waiting_document",
"start_date": null,
"end_date": null,
"total_days": null,
"filled_days": 0,
"total_entries": 0,
"github_repo": null,
"github_username": null,
"certificate_code": "TRN-2026-FMVY-VKTB",
"has_custom_transcript": false,
"transcript_length": 0,
"notebook_supervisor_signed": false,
"notebook_unit_signed": false,
"notebook_approved": false
}
]
+46
View File
@@ -0,0 +1,46 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$interns = \App\Models\CareerApplication::where('type', 'internship')
->whereIn('id', [18, 19, 20, 22, 23, 25, 27, 28, 31, 32])
->with(['journalEntries' => function($q) {
$q->orderBy('day_number', 'asc');
}])->get();
$report = [];
foreach ($interns as $i) {
$entries = [];
$allText = "";
foreach ($i->journalEntries as $e) {
$entries[] = [
'day' => $e->day_number,
'date' => $e->date,
'content' => strip_tags($e->content)
];
$allText .= "\n" . strip_tags($e->content);
}
$report[$i->id] = [
'name' => $i->name,
'email' => $i->email,
'repo' => $i->github_repo,
'github_username' => $i->github_username,
'start_date' => $i->internship_start_date,
'end_date' => $i->internship_end_date,
'total_days' => $i->internship_total_days,
'filled_days' => count($entries),
'entries_sample' => [
'first' => $entries[0] ?? null,
'middle' => $entries[intval(count($entries)/2)] ?? null,
'last' => $entries[count($entries)-1] ?? null,
],
'full_text_length' => strlen($allText),
'all_entries' => $entries
];
}
file_put_contents(__DIR__ . '/detailed_intern_analysis.json', json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo "Generated detailed analysis for " . count($report) . " active/completed interns.\n";
+48
View File
@@ -0,0 +1,48 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$interns = App\Models\CareerApplication::where('type', 'internship')->where('status', 'accepted')->with(['journalEntries' => function($q) {
$q->orderBy('day_number', 'asc');
}])->get();
$analysis = [];
foreach ($interns as $intern) {
if ($intern->journalEntries->count() == 0) continue;
$repoUrl = $intern->github_repo;
$allContent = "";
$commits = [];
foreach ($intern->journalEntries as $entry) {
$allContent .= "\n--- DAY {$entry->day_number} ({$entry->date}) ---\n" . $entry->content . "\n";
// Extract commits from content if available
if (preg_match_all('/\[[0-9:]+\]\s*\[([a-f0-9]+)\]\s*(.+)/i', $entry->content, $matches, PREG_SET_ORDER)) {
foreach ($matches as $m) {
$commits[] = [
'hash' => $m[1],
'msg' => $m[2],
'day' => $entry->day_number
];
}
}
}
$analysis[$intern->name] = [
'id' => $intern->id,
'email' => $intern->email,
'repo' => $repoUrl,
'github_username' => $intern->github_username,
'total_entries' => $intern->journalEntries->count(),
'commits_count' => count($commits),
'commits_sample' => array_slice($commits, -10),
'full_text' => $allContent
];
}
file_put_contents(__DIR__ . '/repo_analysis_data.json', json_encode($analysis, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo "Saved analysis data for " . count($analysis) . " interns.\n";
+838
View File
@@ -0,0 +1,838 @@
{
"approved_total": 58,
"interns": [
{
"id": 18,
"name": "Emre Satıl",
"email": "emresatil72@gmail.com",
"github": "https:\/\/github.com\/Emresatil\/Recycle-Rush-VR\/commits\/feature\/save-manager-and-haptics",
"total_entries": 27,
"filled_entries": 27,
"already_approved": 20,
"newly_approved": 7,
"on_time_count": 24,
"retroactive_count": 3,
"on_time_days": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
13,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27
],
"retroactive_days": [
11,
12,
14
],
"retroactive_details": [
{
"day": 11,
"date": "2026-07-16",
"content_snippet": "11. Gün (16.07.2026) Çalışma RaporuBugün stajımda projemizin"
},
{
"day": 12,
"date": "2026-07-17",
"content_snippet": "12. Gün (17.07.2026) Çalışma RaporuBugün oyunun en önemli hi"
},
{
"day": 14,
"date": "2026-07-21",
"content_snippet": "14. Gün (21.07.2026) Çalışma RaporuBugün, VR projemizin perf"
}
]
},
{
"id": 19,
"name": "Ayşenur Ebrar Gündüz",
"email": "aysenurebrargunduzz@gmail.com",
"github": "https:\/\/github.com\/AysenurGunduz\/vantage-ai",
"total_entries": 15,
"filled_entries": 15,
"already_approved": 8,
"newly_approved": 7,
"on_time_count": 12,
"retroactive_count": 3,
"on_time_days": [
1,
2,
3,
7,
8,
9,
10,
11,
12,
13,
14,
15
],
"retroactive_days": [
4,
5,
6
],
"retroactive_details": [
{
"day": 4,
"date": "2026-07-23",
"content_snippet": "Güne ekip arkadaşlarım ile gerçekleştirdiğimiz toplantı ile "
},
{
"day": 5,
"date": "2026-07-24",
"content_snippet": "Toplantının ardından güne, her gün olduğu gibi yeni bir bran"
},
{
"day": 6,
"date": "2026-07-27",
"content_snippet": "Bugüne yine ekip içi günlük toplantıda Cuma günkü ilerlemeyi"
}
]
},
{
"id": 20,
"name": "Hakan Üzer",
"email": "hakanuzer1@gmail.com",
"github": "https:\/\/github.com\/Emresatil\/Recycle-Rush-VR\/tree\/feature\/ar-golden-spawner-rewards",
"total_entries": 26,
"filled_entries": 26,
"already_approved": 20,
"newly_approved": 6,
"on_time_count": 21,
"retroactive_count": 5,
"on_time_days": [
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
21,
22,
23,
24,
26
],
"retroactive_days": [
1,
2,
19,
20,
25
],
"retroactive_details": [
{
"day": 1,
"date": "2026-07-01",
"content_snippet": "Konu: Unity VR Proje İskeletinin Oluşturulması ve Versiyon K"
},
{
"day": 2,
"date": "2026-07-02",
"content_snippet": "Konu: Unity VR Projenin Sadeleştirlmesi ve XR Altyapısının Y"
},
{
"day": 19,
"date": "2026-07-28",
"content_snippet": "VR Projesinde Sahne Senkronizasyonu, Görsel Post-Processing "
},
{
"day": 20,
"date": "2026-07-29",
"content_snippet": "Oyun Durum Yönetimi (GameState) Optimizasyonu, Dinamik Çevre"
},
{
"day": 25,
"date": "2026-08-05",
"content_snippet": "Bugünkü çalışmalar kapsamında, projemizin VR (Sanal Gerçekli"
}
]
},
{
"id": 22,
"name": "Mustafa emre kaplan",
"email": "mustafaemre027@gmail.com",
"github": "https:\/\/github.com\/mustafaemre027\/securewatch-ai",
"total_entries": 19,
"filled_entries": 19,
"already_approved": 12,
"newly_approved": 7,
"on_time_count": 11,
"retroactive_count": 8,
"on_time_days": [
4,
6,
7,
8,
9,
10,
12,
15,
17,
18,
19
],
"retroactive_days": [
1,
2,
3,
5,
11,
13,
14,
16
],
"retroactive_details": [
{
"day": 1,
"date": "2026-07-13",
"content_snippet": "1. Gün – Proje Planlama ve GitHub AltyapısıProje Tanımı ve P"
},
{
"day": 2,
"date": "2026-07-14",
"content_snippet": "2. Gün – CIC-IDS2017 Veri Seti AnaliziVeri Seti Seçimi ve Ha"
},
{
"day": 3,
"date": "2026-07-16",
"content_snippet": "3. Gün – Sistem Mimarisi TasarımıFonksiyonel Gereksinimler v"
},
{
"day": 5,
"date": "2026-07-20",
"content_snippet": "5. Gün – Kimlik Doğrulama, RBAC ve Audit LogKullanıcı ve Ver"
},
{
"day": 11,
"date": "2026-07-28",
"content_snippet": "Gün 11 – Güvenli Model Tahmini ve Analiz API’siGüvenli Model"
},
{
"day": 13,
"date": "2026-07-30",
"content_snippet": "Gün 13 – Frontend Temeli, Güvenli Kimlik Doğrulama ve Uygula"
},
{
"day": 14,
"date": "2026-07-31",
"content_snippet": "Gün 14 – Analiz Ekranları ve Güvenli CSV İş AkışıAnaliz API "
},
{
"day": 16,
"date": "2026-08-04",
"content_snippet": "Gün 16 – Güvenli Olay Yönetimi Arayüzü ve İş AkışıOlay Yönet"
}
]
},
{
"id": 23,
"name": "Doğukan Kalkan",
"email": "kalkandogukan01@gmail.com",
"github": "https:\/\/github.com\/Dogukan-klkn\/StockRoute",
"total_entries": 20,
"filled_entries": 20,
"already_approved": 20,
"newly_approved": 0,
"on_time_count": 9,
"retroactive_count": 11,
"on_time_days": [
4,
10,
11,
12,
15,
16,
17,
18,
19
],
"retroactive_days": [
1,
2,
3,
5,
6,
7,
8,
9,
13,
14,
20
],
"retroactive_details": [
{
"day": 1,
"date": "2026-06-29",
"content_snippet": "Oryantasyon ve Kurulum Süreçleri: Stajın ilk günü şirket ve "
},
{
"day": 2,
"date": "2026-06-30",
"content_snippet": "Tasarım ve Dokümantasyon Yapısının Kurulumu: Proje planlamas"
},
{
"day": 3,
"date": "2026-07-01",
"content_snippet": "Projenin daha ölçeklenebilir ve tek merkezden yönetilebilir "
},
{
"day": 5,
"date": "2026-07-03",
"content_snippet": "Kimlik Doğrulama ve Güvenlik Altyapısının Kurulması: Sistem "
},
{
"day": 6,
"date": "2026-07-06",
"content_snippet": "Yetki Kontrol Mekanizmasının (Guard) Kurulması: Sistemdeki A"
},
{
"day": 7,
"date": "2026-07-07",
"content_snippet": "Başlangıç Verileri (Seed) ve Onboarding: Sistemin ilk kayıt "
},
{
"day": 8,
"date": "2026-07-08",
"content_snippet": "Veri Doğrulama (DTO) ve Dokümantasyon: Şube oluşturma ve gün"
},
{
"day": 9,
"date": "2026-07-09",
"content_snippet": "Proje Dokümantasyonu: Projenin genel yapısını, kullanılan te"
},
{
"day": 13,
"date": "2026-07-16",
"content_snippet": "Bugün, proje planının 13. gün hedefleri doğrultusunda sistem"
},
{
"day": 14,
"date": "2026-07-17",
"content_snippet": "Gün 14 — Web Yönetim Panelinin KuruluşuBugün itibarıyla Stoc"
},
{
"day": 20,
"date": "2026-07-27",
"content_snippet": "Projenin son geliştirme günü. Bugün iki blok iş yapıldı: mob"
}
]
},
{
"id": 24,
"name": "Elif Çiftepala",
"email": "elifciftepala82@gmail.com",
"github": null,
"total_entries": 0,
"filled_entries": 0,
"already_approved": 0,
"newly_approved": 0,
"on_time_count": 0,
"retroactive_count": 0,
"on_time_days": [],
"retroactive_days": [],
"retroactive_details": []
},
{
"id": 25,
"name": "Eren Kara",
"email": "erenkara1549@gmail.com",
"github": "https:\/\/github.com\/erenkara0\/Smart-E-Commerce-Assistant",
"total_entries": 23,
"filled_entries": 23,
"already_approved": 15,
"newly_approved": 8,
"on_time_count": 8,
"retroactive_count": 15,
"on_time_days": [
6,
10,
12,
13,
18,
19,
22,
23
],
"retroactive_days": [
1,
2,
3,
4,
5,
7,
8,
9,
11,
14,
15,
16,
17,
20,
21
],
"retroactive_details": [
{
"day": 1,
"date": "2026-07-06",
"content_snippet": "On the first day of the internship, the topic for the projec"
},
{
"day": 2,
"date": "2026-07-07",
"content_snippet": "Today, brand and user interface preparation work was carried"
},
{
"day": 3,
"date": "2026-07-08",
"content_snippet": "Today, the foundational development environment for the proj"
},
{
"day": 4,
"date": "2026-07-09",
"content_snippet": "As part of the M2 milestone for the MikroAsistan project, th"
},
{
"day": 5,
"date": "2026-07-10",
"content_snippet": "Today, I focused on the product data structure for the proje"
},
{
"day": 7,
"date": "2026-07-14",
"content_snippet": "Today, as part of the M4 phase, I focused on the RAG-based r"
},
{
"day": 8,
"date": "2026-07-16",
"content_snippet": "Today, I improved the reliability and maintainability of the"
},
{
"day": 9,
"date": "2026-07-17",
"content_snippet": "Today, I implemented session-based chat memory for the Smart"
},
{
"day": 11,
"date": "2026-07-21",
"content_snippet": "Today, I developed the initial interactive chat interface fo"
},
{
"day": 14,
"date": "2026-07-24",
"content_snippet": "Today, I completed the M7 backend testing milestone by setti"
},
{
"day": 15,
"date": "2026-07-27",
"content_snippet": "Today, I prepared the project for final delivery and demonst"
},
{
"day": 16,
"date": "2026-07-28",
"content_snippet": "I finalized the project’s v1.0.0 release, completed the GitH"
},
{
"day": 17,
"date": "2026-07-29",
"content_snippet": "I started the M8 Excel Product Data Foundation milestone. I "
},
{
"day": 20,
"date": "2026-08-03",
"content_snippet": "Yesterday, I implemented the product import and upsert workf"
},
{
"day": 21,
"date": "2026-08-04",
"content_snippet": "Today, I implemented an Excel product import API endpoint fo"
}
]
},
{
"id": 26,
"name": "Ümit Tunç",
"email": "umit.tunc@truncgil.com",
"github": null,
"total_entries": 0,
"filled_entries": 0,
"already_approved": 0,
"newly_approved": 0,
"on_time_count": 0,
"retroactive_count": 0,
"on_time_days": [],
"retroactive_days": [],
"retroactive_details": []
},
{
"id": 27,
"name": "ismet can sezgin",
"email": "ismet.can.sezgin96@erzurum.edu.tr",
"github": "https:\/\/github.com\/ismetcansezgin\/EEG-Flow",
"total_entries": 15,
"filled_entries": 15,
"already_approved": 10,
"newly_approved": 5,
"on_time_count": 4,
"retroactive_count": 11,
"on_time_days": [
5,
6,
7,
9
],
"retroactive_days": [
1,
2,
3,
4,
8,
10,
11,
12,
13,
14,
15
],
"retroactive_details": [
{
"day": 1,
"date": "2026-07-13",
"content_snippet": "Subject of Work:&nbsp;Project Scope Definition, Roadmap Desi"
},
{
"day": 2,
"date": "2026-07-14",
"content_snippet": "Subject of Work:&nbsp;Directory Structure Setup, Environment"
},
{
"day": 3,
"date": "2026-07-16",
"content_snippet": "Subject of Work:&nbsp;EEG CSV Data Loading, Multi-Channel Va"
},
{
"day": 4,
"date": "2026-07-17",
"content_snippet": "Subject of Work: EEG Synthetic Signal Simulator Development "
},
{
"day": 8,
"date": "2026-07-23",
"content_snippet": "Subject of Work:&nbsp;Signal Processing Backend Integration:"
},
{
"day": 10,
"date": "2026-07-27",
"content_snippet": "Subject of Work:&nbsp;Signal Visualization: Chart.js Interac"
},
{
"day": 11,
"date": "2026-07-28",
"content_snippet": "Subject of Work:&nbsp;Feature Engineering Phase: Sliding Win"
},
{
"day": 12,
"date": "2026-07-29",
"content_snippet": "Subject of Work:&nbsp;Feature Engineering REST API: Implemen"
},
{
"day": 13,
"date": "2026-07-30",
"content_snippet": "Subject of Work:&nbsp;Phase 2 Feature Engineering: Signal Ep"
},
{
"day": 14,
"date": "2026-07-31",
"content_snippet": "Subject of Work:&nbsp;Feature Engineering Phase: Implementat"
},
{
"day": 15,
"date": "2026-08-03",
"content_snippet": "Subject of Work:&nbsp;Feature Engine REST API Endpoint, Alph"
}
]
},
{
"id": 28,
"name": "Alesam Baath",
"email": "isambais15@gmail.com",
"github": "https:\/\/github.com\/isambais\/SmartHome-EnergyRL",
"total_entries": 18,
"filled_entries": 18,
"already_approved": 11,
"newly_approved": 7,
"on_time_count": 10,
"retroactive_count": 8,
"on_time_days": [
2,
5,
6,
7,
13,
14,
15,
16,
17,
18
],
"retroactive_days": [
1,
3,
4,
8,
9,
10,
11,
12
],
"retroactive_details": [
{
"day": 1,
"date": "2026-07-13",
"content_snippet": "Bugün Ne Yapıldı?Projenin resmi başlangıcı olarak GitHub üze"
},
{
"day": 3,
"date": "2026-07-16",
"content_snippet": "Bugün Ne Yapıldı?Yol haritasındaki Gün 3 hedefi doğrultusund"
},
{
"day": 4,
"date": "2026-07-17",
"content_snippet": "Bugün Ne Yapıldı?implementation_plan.md Bölüm 8'de projedeki"
},
{
"day": 8,
"date": "2026-07-23",
"content_snippet": "Gün 8 — Gerçek EPIAS Verisi, VecNormalize ve Çok Algoritmali"
},
{
"day": 9,
"date": "2026-07-24",
"content_snippet": "Gün 9 — Gerçek Dünya Ortamı Yeniden Yazımı ve Curriculum Aşa"
},
{
"day": 10,
"date": "2026-07-27",
"content_snippet": "Gün 10 — Pazartesi, 27 Temmuz 2026Öz-Tüketim Bonusu, Hiperpa"
},
{
"day": 11,
"date": "2026-07-28",
"content_snippet": "Gün 11 — Salı, 28 Temmuz 2026Phase 1 → Phase 2 Karşılaştırma"
},
{
"day": 12,
"date": "2026-07-29",
"content_snippet": "Gün 12 — Aşama 3: Hibrit Aksiyon Uzayı ve Ertelenebilir Yük1"
}
]
},
{
"id": 29,
"name": "Mehmet Akif Tunçer",
"email": "akiftuncer0@gmail.com",
"github": null,
"total_entries": 0,
"filled_entries": 0,
"already_approved": 0,
"newly_approved": 0,
"on_time_count": 0,
"retroactive_count": 0,
"on_time_days": [],
"retroactive_days": [],
"retroactive_details": []
},
{
"id": 30,
"name": "Melike Bayer",
"email": "melikebayer09@gmail.com",
"github": null,
"total_entries": 0,
"filled_entries": 0,
"already_approved": 0,
"newly_approved": 0,
"on_time_count": 0,
"retroactive_count": 0,
"on_time_days": [],
"retroactive_days": [],
"retroactive_details": []
},
{
"id": 31,
"name": "Faruk Tazeoğlu",
"email": "faruktazeoglu9@gmail.com",
"github": "https:\/\/github.com\/Faruk-T\/baret",
"total_entries": 17,
"filled_entries": 17,
"already_approved": 11,
"newly_approved": 6,
"on_time_count": 7,
"retroactive_count": 10,
"on_time_days": [
3,
5,
6,
7,
8,
9,
17
],
"retroactive_days": [
1,
2,
4,
10,
11,
12,
13,
14,
15,
16
],
"retroactive_details": [
{
"day": 1,
"date": "2026-07-10",
"content_snippet": "Stajımın ilk gününde şirket içi oryantasyon süreçleri tamaml"
},
{
"day": 2,
"date": "2026-07-13",
"content_snippet": "Baret projesi için kapsamlı implementation_plan.md hazırladı"
},
{
"day": 4,
"date": "2026-07-16",
"content_snippet": "Baret projesinde alıcı akışının 3 temel ekranı (Ana Sayfa, Ü"
},
{
"day": 10,
"date": "2026-07-24",
"content_snippet": "Bugün Baret projesinde 13. kısım kapsamında Supabase Storage"
},
{
"day": 11,
"date": "2026-07-27",
"content_snippet": "27 Temmuz 2026 — Faz 3 Gün 13 kapanışı + Gün 14 (Alıcı ana s"
},
{
"day": 12,
"date": "2026-07-28",
"content_snippet": "Bugün Baret projesinde Faz 3 kapsamındaki Gün 15 paketini ta"
},
{
"day": 13,
"date": "2026-07-29",
"content_snippet": "Bugün Baret projesinde Faz 3 kapsamındaki Gün 15 paketini ta"
},
{
"day": 14,
"date": "2026-07-30",
"content_snippet": "Bugün Baret pazaryeri uygulamasında hem kritik özellik tamam"
},
{
"day": 15,
"date": "2026-07-31",
"content_snippet": "Bugün Baret projesinin canlı teslim ve kapanış günüydü. Uygu"
},
{
"day": 16,
"date": "2026-08-03",
"content_snippet": "day-21-esn-interest branch’i üzerinden go-live baseline’a ES"
}
]
},
{
"id": 32,
"name": "Barış Paşa",
"email": "barispasa460@gmail.com",
"github": "https:\/\/github.com\/baris8138\/UstaFlow_litte",
"total_entries": 8,
"filled_entries": 8,
"already_approved": 3,
"newly_approved": 5,
"on_time_count": 1,
"retroactive_count": 7,
"on_time_days": [
8
],
"retroactive_days": [
1,
2,
3,
4,
5,
6,
7
],
"retroactive_details": [
{
"day": 1,
"date": "2026-07-23",
"content_snippet": "Stajın birinci gününde, teknik servis ve saha ekiplerinin iş"
},
{
"day": 2,
"date": "2026-07-24",
"content_snippet": "Stajın ikinci gününde, UstaFlow Lite projesinin dört haftalı"
},
{
"day": 3,
"date": "2026-07-27",
"content_snippet": "Git Commit Mesajları:[11:52] (41b7ea7) Merge pull request #5"
},
{
"day": 4,
"date": "2026-07-28",
"content_snippet": "Git Commit Mesajları:[18:03] (84cc453) docs(brand): add Usta"
},
{
"day": 5,
"date": "2026-07-29",
"content_snippet": "Git Commit Mesajları:[08:44] (66775b7) feat(database): add u"
},
{
"day": 6,
"date": "2026-07-30",
"content_snippet": "Git Commit Mesajları:[16:35] (1b468e1) chore(auth): install "
},
{
"day": 7,
"date": "2026-07-31",
"content_snippet": "UstaFlow Lite projesinde kimlik doğrulama altyapısının genel"
}
]
}
]
}
+37
View File
@@ -0,0 +1,37 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$interns = \App\Models\CareerApplication::where('type', 'internship')->with(['journalEntries' => function($q) {
$q->orderBy('day_number', 'asc');
}])->get();
$results = [];
foreach ($interns as $i) {
$filledEntries = $i->journalEntries->filter(fn($e) => !empty(trim($e->content)));
$hasTrans = !empty($i->transcript_markdown) && !str_contains($i->transcript_markdown, 'Laravel framework, RESTful API');
$results[] = [
'id' => $i->id,
'name' => $i->name,
'email' => $i->email,
'status' => $i->status,
'start_date' => $i->internship_start_date,
'end_date' => $i->internship_end_date,
'total_days' => $i->internship_total_days,
'filled_days' => $filledEntries->count(),
'total_entries' => $i->journalEntries->count(),
'github_repo' => $i->github_repo,
'github_username' => $i->github_username,
'certificate_code' => $i->certificate_code,
'has_custom_transcript' => $hasTrans,
'transcript_length' => strlen($i->transcript_markdown ?? ''),
'notebook_supervisor_signed' => $i->notebook_supervisor_signed,
'notebook_unit_signed' => $i->notebook_unit_signed,
'notebook_approved' => $i->notebook_approved,
];
}
file_put_contents(__DIR__ . '/all_interns_summary.json', json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo "Saved " . count($results) . " interns to scratch/all_interns_summary.json\n";
+35
View File
@@ -0,0 +1,35 @@
<?php
$content = file_get_contents('resources/views/front/career/intern_dashboard.blade.php');
$lines = explode("\n", $content);
$stack = [];
foreach ($lines as $num => $line) {
preg_match_all('/@(if|elseif|else|endif|foreach|endforeach|for|endfor|while|endwhile|section|endsection|push|endpush)\b/', $line, $matches);
foreach ($matches[1] as $directive) {
if (in_array($directive, ['if', 'foreach', 'for', 'while', 'section', 'push'])) {
$stack[] = ['type' => $directive, 'line' => $num + 1];
} elseif (in_array($directive, ['endif', 'endforeach', 'endfor', 'endwhile', 'endsection', 'endpush'])) {
$mapping = [
'endif' => 'if',
'endforeach' => 'foreach',
'endfor' => 'for',
'endwhile' => 'while',
'endsection' => 'section',
'endpush' => 'push'
];
$expected = $mapping[$directive];
$last = array_pop($stack);
if (!$last || $last['type'] !== $expected) {
echo "Mismatch at line " . ($num + 1) . ": got @" . $directive . ", but last was " . json_encode($last) . "\n";
}
}
}
}
if (!empty($stack)) {
echo "Unclosed directives remaining in stack:\n";
print_r($stack);
} else {
echo "All blade directives matched!\n";
}
+80
View File
@@ -0,0 +1,80 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$weekStart = '2026-08-03';
$weekEnd = '2026-08-07';
$interns = App\Models\CareerApplication::where('type', 'internship')
->with(['journalEntries' => function($q) {
$q->orderBy('date', 'asc');
}])->get();
echo "=== BU HAFTA (03.08.2026 - 07.08.2026) STAJYER KODLAMA DURUMU ===\n\n";
$noCodingActiveInterns = [];
$activeInternsWithCoding = [];
$inactiveInterns = [];
foreach ($interns as $intern) {
// Check if internship is active in this date range
$start = $intern->internship_start_date;
$end = $intern->internship_end_date;
// An intern is active this week if internship started on or before Friday 2026-08-07, and ends on or after Monday 2026-08-03
$isActiveThisWeek = ($start <= $weekEnd && $end >= $weekStart);
// Filter entries for this week
$thisWeekEntries = [];
foreach ($intern->journalEntries as $entry) {
if ($entry->date >= $weekStart && $entry->date <= $weekEnd) {
if (!empty(trim($entry->content))) {
$thisWeekEntries[] = $entry;
}
}
}
$internData = [
'id' => $intern->id,
'name' => $intern->name,
'email' => $intern->email,
'start_date' => $start,
'end_date' => $end,
'is_active' => $isActiveThisWeek,
'this_week_entry_count' => count($thisWeekEntries),
'this_week_dates' => array_map(fn($e) => $e->date, $thisWeekEntries),
'all_entries_count' => $intern->journalEntries->where('content', '!=', '')->count(),
];
if (!$isActiveThisWeek) {
$inactiveInterns[] = $internData;
} else {
if (count($thisWeekEntries) == 0) {
$noCodingActiveInterns[] = $internData;
} else {
$activeInternsWithCoding[] = $internData;
}
}
}
echo "1. STAJ GÜNÜ AKTİF OLUP BU HAFTA HİÇ KODLAMA / DEFTEN GİRİŞİ YAPMAYANLAR:\n";
foreach ($noCodingActiveInterns as $i) {
echo "• " . $i['name'] . " (" . $i['email'] . ")\n";
echo " - Staj Tarihleri: " . $i['start_date'] . " - " . $i['end_date'] . "\n";
echo " - Bu Hafta Doldurulan Gün Sayısı: 0\n";
echo " - Toplam (Tüm Staj Boyunca) Giriş Sayısı: " . $i['all_entries_count'] . "\n\n";
}
echo "2. STAJ GÜNÜ AKTİF OLUP BU HAFTA KODLAMA / DEFTEN GİRİŞİ YAPANLAR:\n";
foreach ($activeInternsWithCoding as $i) {
echo "• " . $i['name'] . " (" . $i['email'] . ")\n";
echo " - Staj Tarihleri: " . $i['start_date'] . " - " . $i['end_date'] . "\n";
echo " - Bu Hafta Doldurulan Gün Sayısı: " . $i['this_week_entry_count'] . " / 5 gün (Tarihler: " . implode(', ', $i['this_week_dates']) . ")\n\n";
}
echo "3. BU HAFTA STAJ DÖNEMİ DIŞINDA / PASİF OLANLAR:\n";
foreach ($inactiveInterns as $i) {
echo "• " . $i['name'] . " (" . $i['email'] . ") [Tarihler: " . $i['start_date'] . " - " . $i['end_date'] . "]\n";
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+22
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More