Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42fcc5759f | |||
| 161d1c86bc | |||
| 4af8900a78 | |||
| abcf105d07 | |||
| d0292af20c | |||
| 26670fcca5 | |||
| f557d4b91c | |||
| 09e92506fd | |||
| 129dae4570 | |||
| f0a9180a54 | |||
| a6c250c13e | |||
| f84f78a138 | |||
| d9c1c825d9 | |||
| 13f8f09524 | |||
| 351b9064af | |||
| edc843fa1c | |||
| 8cf9e0b191 | |||
| 44852bdc95 | |||
| 7b70621c5a | |||
| 4c5cf1d355 | |||
| 5b46b4ac9e | |||
| 32c556a87b | |||
| aa6858bc0f | |||
| 04e62ea9b8 | |||
| 66366aa8b8 | |||
| 0e558fd1dc | |||
| 01be250e6c | |||
| 6614d13be2 | |||
| 61480399b3 | |||
| e3e7ac7f18 | |||
| f1c7e57e43 | |||
| ad50102c42 | |||
| 007da1227d | |||
| 58ae406b36 | |||
| 794f9556de | |||
| 6be7f4442a | |||
| 9361a01c80 | |||
| f9abe3c02a | |||
| efc6b911e7 | |||
| 793a843be7 | |||
| 742303eb73 | |||
| cfce7b8b3f | |||
| 13d12e4f8f | |||
| ba6431e500 | |||
| eca57bf9cc | |||
| 89b81e6ee8 | |||
| fa06ba7545 | |||
| 4eb0a96832 | |||
| 3600cc2334 | |||
| 554f22695d | |||
| af82f47ea0 | |||
| 612a24042b | |||
| 082fb33af3 | |||
| 5b07f385b1 | |||
| aa4a4b3e27 | |||
| b776ebbd26 |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\Pages\CreateCareerApplication;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Pages\EditCareerApplication;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Pages\ListCareerApplications;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Schemas\CareerApplicationForm;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Tables\CareerApplicationsTable;
|
||||
use App\Models\CareerApplication;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CareerApplicationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = CareerApplication::class;
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-user-group';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('career.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('career.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('career.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return CareerApplicationForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return CareerApplicationsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListCareerApplications::route('/'),
|
||||
'create' => CreateCareerApplication::route('/create'),
|
||||
'edit' => EditCareerApplication::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCareerApplication extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CareerApplicationResource::class;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCareerApplications extends ListRecords
|
||||
{
|
||||
protected static string $resource = CareerApplicationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Schemas;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Illuminate\Support\Str;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\View;
|
||||
|
||||
class CareerApplicationForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tab::make('Kişisel ve Başvuru Bilgileri')
|
||||
->icon('heroicon-m-user')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('career.name'))
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
TextInput::make('email')
|
||||
->label(__('career.email'))
|
||||
->email()
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
TextInput::make('phone')
|
||||
->label(__('career.phone'))
|
||||
->disabled(),
|
||||
|
||||
Select::make('status')
|
||||
->label(__('career.status'))
|
||||
->options([
|
||||
'pending' => __('career.pending'),
|
||||
'reviewed' => __('career.reviewed'),
|
||||
'rejected' => __('career.rejected'),
|
||||
'accepted' => __('career.accepted'),
|
||||
'waiting_document' => __('career.waiting_document'),
|
||||
])
|
||||
->required(),
|
||||
|
||||
Textarea::make('message')
|
||||
->label(__('career.message'))
|
||||
->disabled()
|
||||
->columnSpanFull(),
|
||||
])->columns(2),
|
||||
|
||||
Tab::make('Staj Belgeleri & Giriş Bilgileri')
|
||||
->icon('heroicon-m-document-text')
|
||||
->schema([
|
||||
FileUpload::make('cv_path')
|
||||
->label(__('career.cv'))
|
||||
->disk('public')
|
||||
->directory('cvs')
|
||||
->required()
|
||||
->disabled()
|
||||
->downloadable(),
|
||||
|
||||
TextInput::make('username')
|
||||
->label('Kullanıcı Adı')
|
||||
->unique(ignoreRecord: true)
|
||||
->nullable(),
|
||||
|
||||
TextInput::make('password')
|
||||
->label('Şifre')
|
||||
->password()
|
||||
->revealable()
|
||||
->dehydrateStateUsing(fn ($state) => filled($state) ? Hash::make($state) : null)
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->placeholder('Şifreyi değiştirmek istemiyorsanız boş bırakın')
|
||||
->nullable()
|
||||
->suffixAction(
|
||||
Action::make('generatePassword')
|
||||
->icon('heroicon-m-arrow-path')
|
||||
->action(fn (Set $set) => $set('password', Str::random(12)))
|
||||
),
|
||||
|
||||
FileUpload::make('to_be_signed_internship_form_path')
|
||||
->label('İmzalanacak Staj Formu (Stajyerden)')
|
||||
->disk('public')
|
||||
->directory('to_be_signed_interns')
|
||||
->downloadable()
|
||||
->nullable(),
|
||||
|
||||
FileUpload::make('signed_internship_form_path')
|
||||
->label('İmzalı Staj Formu')
|
||||
->disk('public')
|
||||
->directory('signed_interns')
|
||||
->downloadable()
|
||||
->nullable(),
|
||||
|
||||
DatePicker::make('internship_start_date')
|
||||
->label('Staj Başlangıç Tarihi')
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, $get, Set $set) {
|
||||
if ($state && $get('internship_total_days')) {
|
||||
self::calculateEndDate($state, $get('internship_total_days'), $set);
|
||||
} elseif ($state && $get('internship_end_date')) {
|
||||
self::calculateTotalDays($state, $get('internship_end_date'), $set);
|
||||
}
|
||||
})
|
||||
->nullable(),
|
||||
|
||||
DatePicker::make('internship_end_date')
|
||||
->label('Staj Bitiş Tarihi')
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateTotalDays($get('internship_start_date'), $state, $set))
|
||||
->nullable(),
|
||||
|
||||
TextInput::make('internship_total_days')
|
||||
->label('Toplam Staj Süresi (İş Günü)')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateEndDate($get('internship_start_date'), $state, $set))
|
||||
->nullable(),
|
||||
])->columns(2),
|
||||
|
||||
Tab::make('Staj Günlüğü')
|
||||
->icon('heroicon-m-squares-plus')
|
||||
->schema([
|
||||
TextInput::make('github_repo')
|
||||
->label('GitHub Depo URL\'si')
|
||||
->url()
|
||||
->nullable()
|
||||
->live(),
|
||||
|
||||
// View::make('filament.components.intern-journal-timeline')
|
||||
// ->columnSpanFull()
|
||||
])
|
||||
])->columnSpanFull()
|
||||
]);
|
||||
}
|
||||
|
||||
public static function calculateTotalDays($start, $end, Set $set): void
|
||||
{
|
||||
if (!$start || !$end) {
|
||||
$set('internship_total_days', null);
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = \Carbon\Carbon::parse($start);
|
||||
$endDate = \Carbon\Carbon::parse($end);
|
||||
|
||||
if ($startDate->gt($endDate)) {
|
||||
$set('internship_total_days', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
$days = 0;
|
||||
while ($startDate->lte($endDate)) {
|
||||
if (!$startDate->isWeekend()) {
|
||||
$days++;
|
||||
}
|
||||
$startDate->addDay();
|
||||
}
|
||||
|
||||
$set('internship_total_days', $days);
|
||||
}
|
||||
|
||||
public static function calculateEndDate($start, $totalDays, Set $set): void
|
||||
{
|
||||
if (!$start || !$totalDays || $totalDays <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = \Carbon\Carbon::parse($start);
|
||||
$daysToAdd = intval($totalDays);
|
||||
|
||||
$endDate = $startDate->copy();
|
||||
$count = 0;
|
||||
$temp = $startDate->copy();
|
||||
|
||||
while ($count < $daysToAdd) {
|
||||
if ($temp->isWeekend()) {
|
||||
$temp->addDay();
|
||||
continue;
|
||||
}
|
||||
$endDate = $temp->copy();
|
||||
$temp->addDay();
|
||||
$count++;
|
||||
}
|
||||
|
||||
$set('internship_end_date', $endDate->format('Y-m-d'));
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Tables;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CareerApplicationsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(__('career.name'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('email')
|
||||
->label(__('career.email'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('phone')
|
||||
->label(__('career.phone'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('type')
|
||||
->label(__('career.type'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'job' => 'success',
|
||||
'internship' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label(__('career.status'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'gray',
|
||||
'reviewed' => 'info',
|
||||
'rejected' => 'danger',
|
||||
'accepted' => 'success',
|
||||
'waiting_document' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
|
||||
|
||||
TextColumn::make('git_knowledge')
|
||||
->label(__('career.git_knowledge'))
|
||||
->badge()
|
||||
->color(fn ($state) => $state ? 'success' : 'danger')
|
||||
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
|
||||
|
||||
TextColumn::make('ai_usage')
|
||||
->label(__('career.ai_usage'))
|
||||
->badge()
|
||||
->color(fn ($state) => $state ? 'success' : 'danger')
|
||||
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('career.created_at'))
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->label(__('career.status'))
|
||||
->options([
|
||||
'pending' => __('career.pending'),
|
||||
'reviewed' => __('career.reviewed'),
|
||||
'rejected' => __('career.rejected'),
|
||||
'accepted' => __('career.accepted'),
|
||||
'waiting_document' => __('career.waiting_document'),
|
||||
]),
|
||||
SelectFilter::make('type')
|
||||
->label(__('career.type'))
|
||||
->options([
|
||||
'job' => __('career.job'),
|
||||
'internship' => __('career.internship'),
|
||||
]),
|
||||
])
|
||||
->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('download_nda')
|
||||
->label(__('career.nda'))
|
||||
->icon('heroicon-o-shield-check')
|
||||
->url(fn ($record) => $record->nda_path ? Storage::disk('public')->url($record->nda_path) : null)
|
||||
->visible(fn ($record) => $record->nda_path !== null)
|
||||
->openUrlInNewTab(),
|
||||
Action::make('download_contract')
|
||||
->label(__('career.contract'))
|
||||
->icon('heroicon-o-document-text')
|
||||
->url(fn ($record) => $record->contract_path ? Storage::disk('public')->url($record->contract_path) : null)
|
||||
->visible(fn ($record) => $record->contract_path !== null)
|
||||
->openUrlInNewTab(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\InternApplications;
|
||||
|
||||
use App\Models\CareerApplication;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\MarkdownEditor;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Livewire;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class InternApplicationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = CareerApplication::class;
|
||||
|
||||
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-academic-cap';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('career.internship_title', ['default' => 'Staj Başvuruları']);
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('career.internship', ['default' => 'Staj Başvurusu']);
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('career.internship_title', ['default' => 'Staj Başvuruları']);
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getEloquentQuery()->where('type', 'internship');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tab::make('Kişisel ve Başvuru Bilgileri')
|
||||
->icon('heroicon-m-user')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('career.name'))
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
TextInput::make('email')
|
||||
->label(__('career.email'))
|
||||
->email()
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
TextInput::make('phone')
|
||||
->label(__('career.phone'))
|
||||
->disabled(),
|
||||
|
||||
Select::make('status')
|
||||
->label(__('career.status'))
|
||||
->options([
|
||||
'pending' => __('career.pending'),
|
||||
'reviewed' => __('career.reviewed'),
|
||||
'rejected' => __('career.rejected'),
|
||||
'accepted' => __('career.accepted'),
|
||||
'waiting_document' => __('career.waiting_document'),
|
||||
])
|
||||
->required(),
|
||||
|
||||
Textarea::make('message')
|
||||
->label(__('career.message'))
|
||||
->disabled()
|
||||
->columnSpanFull(),
|
||||
])->columns(2),
|
||||
|
||||
Tab::make('Staj Belgeleri & Giriş Bilgileri')
|
||||
->icon('heroicon-m-document-text')
|
||||
->schema([
|
||||
FileUpload::make('cv_path')
|
||||
->label(__('career.cv'))
|
||||
->disk('public')
|
||||
->directory('cvs')
|
||||
->required()
|
||||
->disabled()
|
||||
->downloadable(),
|
||||
|
||||
TextInput::make('username')
|
||||
->label('Kullanıcı Adı')
|
||||
->default(fn ($record) => $record?->email)
|
||||
->disabled()
|
||||
->dehydrated()
|
||||
->autocomplete('new-username'),
|
||||
|
||||
TextInput::make('password')
|
||||
->label('Şifre')
|
||||
->password()
|
||||
->revealable()
|
||||
->autocomplete('new-password')
|
||||
->formatStateUsing(fn () => null)
|
||||
->dehydrateStateUsing(fn ($state) => filled($state) ? Hash::make($state) : null)
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->placeholder('Şifreyi değiştirmek istemiyorsanız boş bırakın')
|
||||
->nullable()
|
||||
->suffixAction(
|
||||
\Filament\Actions\Action::make('generatePassword')
|
||||
->icon('heroicon-m-arrow-path')
|
||||
->action(fn (Set $set) => $set('password', Str::random(12)))
|
||||
),
|
||||
|
||||
FileUpload::make('to_be_signed_internship_form_path')
|
||||
->label('İmzalanacak Staj Formu (Stajyerden)')
|
||||
->disk('public')
|
||||
->directory('to_be_signed_interns')
|
||||
->downloadable()
|
||||
->nullable(),
|
||||
|
||||
FileUpload::make('signed_internship_form_path')
|
||||
->label('İmzalı Staj Formu')
|
||||
->disk('public')
|
||||
->directory('signed_interns')
|
||||
->downloadable()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if ($state) {
|
||||
$set('status', 'accepted');
|
||||
}
|
||||
})
|
||||
->nullable(),
|
||||
|
||||
DatePicker::make('internship_start_date')
|
||||
->label('Staj Başlangıç Tarihi')
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, $get, Set $set) {
|
||||
if ($state && $get('internship_total_days')) {
|
||||
self::calculateEndDate($state, $get('internship_total_days'), $set);
|
||||
} elseif ($state && $get('internship_end_date')) {
|
||||
self::calculateTotalDays($state, $get('internship_end_date'), $set);
|
||||
}
|
||||
})
|
||||
->nullable(),
|
||||
|
||||
DatePicker::make('internship_end_date')
|
||||
->label('Staj Bitiş Tarihi')
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateTotalDays($get('internship_start_date'), $state, $set))
|
||||
->nullable(),
|
||||
|
||||
TextInput::make('internship_total_days')
|
||||
->label('Toplam Staj Süresi (İş Günü)')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateEndDate($get('internship_start_date'), $state, $set))
|
||||
->nullable(),
|
||||
])->columns(2),
|
||||
|
||||
Tab::make('Staj Günlüğü')
|
||||
->icon('heroicon-m-squares-plus')
|
||||
->schema([
|
||||
TextInput::make('github_repo')
|
||||
->label('GitHub Depo URL\'si')
|
||||
->url()
|
||||
->nullable()
|
||||
->live(),
|
||||
|
||||
Livewire::make(\App\Livewire\InternJournalTimeline::class)
|
||||
->columnSpanFull()
|
||||
]),
|
||||
|
||||
Tab::make('Staj Defteri & Onay')
|
||||
->icon('heroicon-o-book-open')
|
||||
->schema([
|
||||
\Filament\Forms\Components\Placeholder::make('notebook_view')
|
||||
->label('Doldurulan Staj Defteri')
|
||||
->content(function ($record) {
|
||||
if (!$record) return 'Henüz başvuru bulunmamaktadır.';
|
||||
$days = \App\Http\Controllers\CareerController::getInternshipDates($record->internship_start_date, $record->internship_total_days);
|
||||
if (empty($days)) return 'Staj başlangıç tarihi veya süresi girilmemiş.';
|
||||
|
||||
$saved = $record->journalEntries()->get()->keyBy('day_number');
|
||||
|
||||
$html = '<style>
|
||||
.rich-text-content p { margin-bottom: 8px; }
|
||||
.rich-text-content ul { list-style-type: disc; padding-left: 20px; margin-bottom: 8px; }
|
||||
.rich-text-content ol { list-style-type: decimal; padding-left: 20px; margin-bottom: 8px; }
|
||||
.rich-text-content li { margin-bottom: 4px; }
|
||||
</style>';
|
||||
$html .= '<div class="space-y-4" style="max-height: 400px; overflow-y: auto; padding-right: 10px; border: 1px solid #cbd5e1; border-radius: 8px; padding: 15px;">';
|
||||
foreach ($days as $d) {
|
||||
$dayNum = $d['day_number'];
|
||||
$dateF = $d['formatted_date'];
|
||||
$entry = $saved->get($dayNum);
|
||||
$content = $entry ? $entry->content : '';
|
||||
$isRetro = $entry ? $entry->is_retroactive : false;
|
||||
$updatedAt = $entry ? $entry->updated_at->format('d.m.Y H:i') : null;
|
||||
|
||||
$html .= '<div style="margin-bottom: 12px; padding: 12px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc;">';
|
||||
$html .= ' <div style="display:flex; justify-content:between; font-size:11px; font-weight:700; color:#475569; border-bottom:1px solid #e2e8f0; padding-bottom:6px; margin-bottom:8px;">';
|
||||
$html .= ' <span style="font-weight: 800; color: #2563eb;">' . $dayNum . '. Gün Raporu</span>';
|
||||
if ($isRetro) {
|
||||
$html .= ' <span style="margin-left: 10px; background: #fee2e2; color: #991b1b; padding: 1px 6px; border-radius: 4px; font-size: 9px; font-weight: 800;">GERİYE DÖNÜK KAYIT</span>';
|
||||
}
|
||||
if ($updatedAt) {
|
||||
$html .= ' <span style="margin-left: auto;">Son Güncelleme: ' . $updatedAt . '</span>';
|
||||
} else {
|
||||
$html .= ' <span style="margin-left: auto;">' . $dateF . '</span>';
|
||||
}
|
||||
$html .= ' </div>';
|
||||
|
||||
$cleanContent = $content ? strip_tags($content, ['p', 'strong', 'ul', 'li', 'em', 'br', 'b', 'i', 'ol', 'span']) : '<em style="color:#94a3b8;">Rapor yazılmamış</em>';
|
||||
$html .= ' <div class="rich-text-content" style="font-size:12px; color:#1e293b; line-height:1.5;">' . $cleanContent . '</div>';
|
||||
|
||||
if ($entry && trim($content) !== '') {
|
||||
$supApproved = $entry->supervisor_approved;
|
||||
$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>';
|
||||
$html .= ' <a href="' . route('intern.print-journal') . '?size=a5&intern_id=' . $record->id . '" target="_blank" style="display:inline-flex; align-items:center; justify-content:center; padding: 8px 16px; background:#4b5563; color:white; border-radius:8px; font-weight:bold; font-size:12px; text-decoration:none; box-shadow: 0 1px 3px rgba(75, 85, 99, 0.2);">A5 Defteri Önizle / Yazdır</a>';
|
||||
$html .= '</div>';
|
||||
|
||||
return new \Illuminate\Support\HtmlString($html);
|
||||
})
|
||||
->columnSpanFull(),
|
||||
|
||||
\Filament\Schemas\Components\Section::make('Onay ve İmza Bilgileri')
|
||||
->schema([
|
||||
\Filament\Forms\Components\Toggle::make('notebook_supervisor_signed')
|
||||
->label('Staj Sorumlusu İmzala / Onayla')
|
||||
->live(),
|
||||
TextInput::make('notebook_supervisor_name')
|
||||
->label('Staj Sorumlusu Adı / Ünvanı')
|
||||
->placeholder('Örn: Alperen Trunç')
|
||||
->default('Alperen Trunç'),
|
||||
\Filament\Forms\Components\Toggle::make('notebook_approved')
|
||||
->label('Staj Defterini Genel Olarak Onayla')
|
||||
->columnSpanFull(),
|
||||
])->columns(2),
|
||||
]),
|
||||
|
||||
Tab::make('Sertifika & Transkript')
|
||||
->icon('heroicon-o-academic-cap')
|
||||
->schema([
|
||||
TextInput::make('certificate_code')
|
||||
->label('Doğrulama Kodu')
|
||||
->helperText('Belge kaydedildiğinde benzersiz doğrulama kodu otomatik olarak üretilir.')
|
||||
->readonly()
|
||||
->nullable(),
|
||||
|
||||
MarkdownEditor::make('transcript_markdown')
|
||||
->label('Akademik Transkript (Markdown)')
|
||||
->columnSpanFull()
|
||||
->default(function () {
|
||||
return "### STAJ AKADEMİK TRANSKRİPTİ VE PERFORMANS RAPORU\n\n" .
|
||||
"#### 🛠️ Deneyimlenen Teknolojiler ve Kazanımlar\n" .
|
||||
"| Modül / Çalışma Alanı | Kullanılan Teknolojiler / Araçlar | Değerlendirme |\n" .
|
||||
"| --- | --- | --- |\n" .
|
||||
"| Backend Mimari & API | Laravel framework, RESTful API, MySQL | Başarılı |\n" .
|
||||
"| Arayüz & UI/UX Uygulamaları | Flutter, CSS, Glassmorphic Tasarım Prensipleri | Üstün Başarı |\n" .
|
||||
"| Masaüstü & Sistem Entegrasyonu | Electron.js, Git / GitHub | Başarılı |\n" .
|
||||
"| Takım Çalışması & Proje Yönetimi | Agile / Scrum, Slack, JIRA | Başarılı |\n\n" .
|
||||
"#### 📊 Performans Değerlendirme Kriterleri\n" .
|
||||
"| Değerlendirme Kriteri | Puan (100 Üzerinden) | Harf Notu |\n" .
|
||||
"| --- | --- | --- |\n" .
|
||||
"| Teknik Sorumluluk ve Görev Bilinci | 95 | AA |\n" .
|
||||
"| Problem Çözme ve Analitik Düşünme | 90 | BA |\n" .
|
||||
"| Ekip Çalışması ve İletişim Uyum | 95 | AA |\n" .
|
||||
"| Öğrenme Hızı ve Adaptasyon | 100 | AA |\n" .
|
||||
"| **GENEL BAŞARI ORTALAMASI** | **95.00** | **AA (Mükemmel)** |\n\n" .
|
||||
"#### 📝 Danışman Görüşü ve Değerlendirme Notu\n" .
|
||||
"\"Stajyerimiz, staj süresi boyunca kendisine verilen görevleri büyük bir titizlikle yerine getirmiştir. Özellikle karşılaştığı teknik problemlere getirdiği pratik çözümler ve yeni teknolojileri öğrenme isteği takdir edilmeye değerdir. Kurumumuz bünyesinde yürüttüğümüz projelere sağladığı katkılardan ötürü teşekkür eder, profesyonel kariyerinde başarılar dileriz.\"";
|
||||
}),
|
||||
])->columns(1)
|
||||
])->columnSpanFull()
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(__('career.name'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('email')
|
||||
->label(__('career.email'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('phone')
|
||||
->label(__('career.phone'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label(__('career.status'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'gray',
|
||||
'reviewed' => 'info',
|
||||
'rejected' => 'danger',
|
||||
'accepted' => 'success',
|
||||
'waiting_document' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
|
||||
|
||||
TextColumn::make('certificate_code')
|
||||
->label('Sertifika Kodu')
|
||||
->searchable()
|
||||
->placeholder('Yok'),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('career.created_at'))
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->label(__('career.status'))
|
||||
->options([
|
||||
'pending' => __('career.pending'),
|
||||
'reviewed' => __('career.reviewed'),
|
||||
'rejected' => __('career.rejected'),
|
||||
'accepted' => __('career.accepted'),
|
||||
'waiting_document' => __('career.waiting_document'),
|
||||
]),
|
||||
])
|
||||
->actions([
|
||||
Action::make('download_cv')
|
||||
->label(__('career.download_cv'))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
|
||||
->openUrlInNewTab(),
|
||||
Action::make('download_signed_form')
|
||||
->label('İmzalı Form İndir')
|
||||
->icon('heroicon-o-document-check')
|
||||
->url(fn ($record) => $record->signed_internship_form_path ? Storage::disk('public')->url($record->signed_internship_form_path) : null)
|
||||
->visible(fn ($record) => !empty($record->signed_internship_form_path))
|
||||
->openUrlInNewTab(),
|
||||
Action::make('view_certificate')
|
||||
->label('Sertifika Doğrulama')
|
||||
->icon('heroicon-o-academic-cap')
|
||||
->color('success')
|
||||
->url(fn ($record) => $record->certificate_code ? route('internship.verify', $record->certificate_code) : null)
|
||||
->visible(fn ($record) => !empty($record->certificate_code))
|
||||
->openUrlInNewTab(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function calculateTotalDays($start, $end, Set $set): void
|
||||
{
|
||||
if (!$start || !$end) {
|
||||
$set('internship_total_days', null);
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = \Carbon\Carbon::parse($start);
|
||||
$endDate = \Carbon\Carbon::parse($end);
|
||||
|
||||
if ($startDate->gt($endDate)) {
|
||||
$set('internship_total_days', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
$days = 0;
|
||||
while ($startDate->lte($endDate)) {
|
||||
if (!$startDate->isWeekend()) {
|
||||
$days++;
|
||||
}
|
||||
$startDate->addDay();
|
||||
}
|
||||
|
||||
$set('internship_total_days', $days);
|
||||
}
|
||||
|
||||
public static function calculateEndDate($start, $totalDays, Set $set): void
|
||||
{
|
||||
if (!$start || !$totalDays || $totalDays <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = \Carbon\Carbon::parse($start);
|
||||
$daysToAdd = intval($totalDays);
|
||||
|
||||
$endDate = $startDate->copy();
|
||||
$count = 0;
|
||||
$temp = $startDate->copy();
|
||||
|
||||
while ($count < $daysToAdd) {
|
||||
if ($temp->isWeekend()) {
|
||||
$temp->addDay();
|
||||
continue;
|
||||
}
|
||||
$endDate = $temp->copy();
|
||||
$temp->addDay();
|
||||
$count++;
|
||||
}
|
||||
|
||||
$set('internship_end_date', $endDate->format('Y-m-d'));
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListInternApplications::route('/'),
|
||||
'create' => Pages\CreateInternApplication::route('/create'),
|
||||
'edit' => Pages\EditInternApplication::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\InternApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\InternApplications\InternApplicationResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateInternApplication extends CreateRecord
|
||||
{
|
||||
protected static string $resource = InternApplicationResource::class;
|
||||
}
|
||||
+4
-4
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
|
||||
namespace App\Filament\Admin\Resources\InternApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
|
||||
use App\Filament\Admin\Resources\InternApplications\InternApplicationResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCareerApplication extends EditRecord
|
||||
class EditInternApplication extends EditRecord
|
||||
{
|
||||
protected static string $resource = CareerApplicationResource::class;
|
||||
protected static string $resource = InternApplicationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\InternApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\InternApplications\InternApplicationResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListInternApplications extends ListRecords
|
||||
{
|
||||
protected static string $resource = InternApplicationResource::class;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// Bu bileşen devre dışı bırakılmıştır.
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\JobApplications;
|
||||
|
||||
use App\Models\CareerApplication;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class JobApplicationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = CareerApplication::class;
|
||||
|
||||
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-briefcase';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('career.job_application_title', ['default' => 'İş Başvuruları']);
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('career.job', ['default' => 'İş Başvurusu']);
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('career.job_application_title', ['default' => 'İş Başvuruları']);
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getEloquentQuery()->where('type', 'job');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->label(__('career.name'))
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
TextInput::make('email')
|
||||
->label(__('career.email'))
|
||||
->email()
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
TextInput::make('phone')
|
||||
->label(__('career.phone'))
|
||||
->disabled(),
|
||||
|
||||
Select::make('status')
|
||||
->label(__('career.status'))
|
||||
->options([
|
||||
'pending' => __('career.pending'),
|
||||
'reviewed' => __('career.reviewed'),
|
||||
'rejected' => __('career.rejected'),
|
||||
'accepted' => __('career.accepted'),
|
||||
'waiting_document' => __('career.waiting_document'),
|
||||
])
|
||||
->required(),
|
||||
|
||||
FileUpload::make('cv_path')
|
||||
->label(__('career.cv'))
|
||||
->disk('public')
|
||||
->directory('cvs')
|
||||
->required()
|
||||
->disabled()
|
||||
->downloadable(),
|
||||
|
||||
FileUpload::make('nda_path')
|
||||
->label(__('career.nda'))
|
||||
->disk('public')
|
||||
->directory('ndas')
|
||||
->disabled()
|
||||
->downloadable(),
|
||||
|
||||
FileUpload::make('contract_path')
|
||||
->label(__('career.contract'))
|
||||
->disk('public')
|
||||
->directory('contracts')
|
||||
->disabled()
|
||||
->downloadable(),
|
||||
|
||||
FileUpload::make('id_photocopy_path')
|
||||
->label(__('career.id_photocopy', ['default' => 'Kimlik Fotokopisi']))
|
||||
->disk('public')
|
||||
->directory('id_photocopies')
|
||||
->disabled()
|
||||
->downloadable(),
|
||||
|
||||
Toggle::make('git_knowledge')
|
||||
->label(__('career.git_knowledge'))
|
||||
->disabled(),
|
||||
|
||||
Toggle::make('ai_usage')
|
||||
->label(__('career.ai_usage'))
|
||||
->disabled(),
|
||||
|
||||
Textarea::make('message')
|
||||
->label(__('career.message'))
|
||||
->disabled()
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(__('career.name'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('email')
|
||||
->label(__('career.email'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('phone')
|
||||
->label(__('career.phone'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label(__('career.status'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'gray',
|
||||
'reviewed' => 'info',
|
||||
'rejected' => 'danger',
|
||||
'accepted' => 'success',
|
||||
'waiting_document' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
|
||||
|
||||
TextColumn::make('git_knowledge')
|
||||
->label(__('career.git_knowledge'))
|
||||
->badge()
|
||||
->color(fn ($state) => $state ? 'success' : 'danger')
|
||||
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
|
||||
|
||||
TextColumn::make('ai_usage')
|
||||
->label(__('career.ai_usage'))
|
||||
->badge()
|
||||
->color(fn ($state) => $state ? 'success' : 'danger')
|
||||
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('career.created_at'))
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->label(__('career.status'))
|
||||
->options([
|
||||
'pending' => __('career.pending'),
|
||||
'reviewed' => __('career.reviewed'),
|
||||
'rejected' => __('career.rejected'),
|
||||
'accepted' => __('career.accepted'),
|
||||
'waiting_document' => __('career.waiting_document'),
|
||||
]),
|
||||
])
|
||||
->actions([
|
||||
Action::make('download_cv')
|
||||
->label(__('career.download_cv'))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
|
||||
->openUrlInNewTab(),
|
||||
Action::make('download_nda')
|
||||
->label(__('career.nda'))
|
||||
->icon('heroicon-o-shield-check')
|
||||
->url(fn ($record) => $record->nda_path ? Storage::disk('public')->url($record->nda_path) : null)
|
||||
->visible(fn ($record) => $record->nda_path !== null)
|
||||
->openUrlInNewTab(),
|
||||
Action::make('download_contract')
|
||||
->label(__('career.contract'))
|
||||
->icon('heroicon-o-document-text')
|
||||
->url(fn ($record) => $record->contract_path ? Storage::disk('public')->url($record->contract_path) : null)
|
||||
->visible(fn ($record) => $record->contract_path !== null)
|
||||
->openUrlInNewTab(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListJobApplications::route('/'),
|
||||
'create' => Pages\CreateJobApplication::route('/create'),
|
||||
'edit' => Pages\EditJobApplication::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\JobApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\JobApplications\JobApplicationResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateJobApplication extends CreateRecord
|
||||
{
|
||||
protected static string $resource = JobApplicationResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\JobApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\JobApplications\JobApplicationResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditJobApplication extends EditRecord
|
||||
{
|
||||
protected static string $resource = JobApplicationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\JobApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\JobApplications\JobApplicationResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListJobApplications extends ListRecords
|
||||
{
|
||||
protected static string $resource = JobApplicationResource::class;
|
||||
}
|
||||
@@ -122,8 +122,13 @@ 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');
|
||||
|
||||
return view('front.career.intern_dashboard', [
|
||||
'intern' => $intern,
|
||||
'days' => $days,
|
||||
'savedEntries' => $savedEntries,
|
||||
'meta' => [
|
||||
'title' => 'Stajyer Paneli',
|
||||
'description' => 'Belgelerinizi buradan indirebilirsiniz.',
|
||||
@@ -186,6 +191,7 @@ class CareerController extends Controller
|
||||
'internship_start_date' => $request->internship_start_date,
|
||||
'internship_end_date' => $endDate->format('Y-m-d'),
|
||||
'internship_total_days' => $daysToAdd,
|
||||
'status' => 'waiting_document',
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', 'İmzalanacak staj formunuz ve staj tarihleri başarıyla kaydedildi.');
|
||||
@@ -202,31 +208,51 @@ 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()
|
||||
{
|
||||
if (!session()->has('intern_id')) {
|
||||
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
|
||||
if (request()->has('intern_id') && auth()->check() && auth()->user()->hasRole('super_admin')) {
|
||||
$internId = request('intern_id');
|
||||
} else {
|
||||
if (!session()->has('intern_id')) {
|
||||
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
|
||||
}
|
||||
$internId = session('intern_id');
|
||||
}
|
||||
$intern = CareerApplication::findOrFail(session('intern_id'));
|
||||
|
||||
$intern = CareerApplication::findOrFail($internId);
|
||||
$repo = $intern->github_repo;
|
||||
|
||||
if (!$repo) {
|
||||
return redirect()->back()->with('error', 'Lütfen önce Github deposunu tanımlayın.');
|
||||
}
|
||||
@@ -312,4 +338,407 @@ class CareerController extends Controller
|
||||
session()->forget('intern_id');
|
||||
return redirect()->route('intern.login')->with('success', 'Başarıyla çıkış yapıldı.');
|
||||
}
|
||||
|
||||
public function verifyCertificate($code)
|
||||
{
|
||||
$application = CareerApplication::where('certificate_code', $code)
|
||||
->where('type', 'internship')
|
||||
->firstOrFail();
|
||||
|
||||
$days = self::getInternshipDates($application->internship_start_date, $application->internship_total_days);
|
||||
$savedEntries = $application->journalEntries()->get()->keyBy('day_number');
|
||||
|
||||
return view('front.career.verify', [
|
||||
'application' => $application,
|
||||
'days' => $days,
|
||||
'savedEntries' => $savedEntries,
|
||||
'meta' => [
|
||||
'title' => 'Staj Bitirme Sertifikası Doğrulama - ' . $application->name,
|
||||
'description' => $application->name . ' isimli stajyerimizin staj bitirme sertifikası ve performans raporu doğrulama sayfası.',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function internAdminLoginForm()
|
||||
{
|
||||
if (auth()->check() && auth()->user()->hasRole('super_admin')) {
|
||||
return redirect()->route('intern.admin.dashboard');
|
||||
}
|
||||
return view('front.career.admin_login', [
|
||||
'meta' => [
|
||||
'title' => 'Yönetici Girişi - Staj Takip',
|
||||
'description' => 'Stajyerleri yönetmek için giriş yapın.',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function internAdminLogin(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
if (auth()->attempt($request->only('email', 'password'))) {
|
||||
if (auth()->user()->hasRole('super_admin')) {
|
||||
return redirect()->route('intern.admin.dashboard')->with('success', 'Yönetici girişi başarıyla gerçekleştirildi.');
|
||||
}
|
||||
|
||||
auth()->logout();
|
||||
return redirect()->back()
|
||||
->withInput($request->only('email'))
|
||||
->withErrors([
|
||||
'email' => 'Bu panele erişmek için super_admin yetkinizin olması gerekir.',
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->withInput($request->only('email'))
|
||||
->withErrors([
|
||||
'email' => 'Girdiğiniz bilgiler eşleşmedi veya hatalı.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function internAdminDashboard()
|
||||
{
|
||||
if (!auth()->check() || !auth()->user()->hasRole('super_admin')) {
|
||||
return redirect()->route('intern.admin.login')->with('error', 'Lütfen önce yönetici olarak giriş yapın.');
|
||||
}
|
||||
|
||||
$allInterns = CareerApplication::where('type', 'internship')
|
||||
->with(['journalEntries'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
$ganttInterns = CareerApplication::where('type', 'internship')
|
||||
->where('status', 'accepted')
|
||||
->whereNotNull('internship_start_date')
|
||||
->whereNotNull('internship_end_date')
|
||||
->orderBy('internship_start_date', 'asc')
|
||||
->get()
|
||||
->map(function ($intern) {
|
||||
return [
|
||||
'id' => $intern->id,
|
||||
'parentId' => null,
|
||||
'title' => $intern->name,
|
||||
'start' => $intern->internship_start_date,
|
||||
'end' => $intern->internship_end_date,
|
||||
'progress' => 100
|
||||
];
|
||||
});
|
||||
|
||||
return view('front.career.admin_dashboard', [
|
||||
'allInterns' => $allInterns,
|
||||
'ganttInterns' => $ganttInterns,
|
||||
'meta' => [
|
||||
'title' => 'Staj Yönetici Paneli',
|
||||
'description' => 'Stajyer bilgilerini ve takvimlerini izleyin.',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function internAdminLogout()
|
||||
{
|
||||
auth()->logout();
|
||||
return redirect()->route('intern.admin.login')->with('success', 'Yönetici oturumu sonlandırıldı.');
|
||||
}
|
||||
|
||||
public static function getInternshipDates($startDate, $totalDays)
|
||||
{
|
||||
if (!$startDate || !$totalDays || $totalDays <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$dates = [];
|
||||
$temp = \Carbon\Carbon::parse($startDate);
|
||||
$daysToAdd = intval($totalDays);
|
||||
$count = 0;
|
||||
|
||||
while ($count < $daysToAdd) {
|
||||
if ($temp->isWeekend()) {
|
||||
$temp->addDay();
|
||||
continue;
|
||||
}
|
||||
$dates[] = [
|
||||
'day_number' => $count + 1,
|
||||
'date' => $temp->format('Y-m-d'),
|
||||
'formatted_date' => $temp->format('d.m.Y'),
|
||||
];
|
||||
$temp->addDay();
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
public function saveJournalEntry(Request $request)
|
||||
{
|
||||
if (!session()->has('intern_id')) {
|
||||
return response()->json(['success' => false, 'message' => 'Lütfen giriş yapın.'], 401);
|
||||
}
|
||||
|
||||
$intern = CareerApplication::findOrFail(session('intern_id'));
|
||||
|
||||
$request->validate([
|
||||
'day_number' => 'required|integer|min:1',
|
||||
'date' => 'required|date',
|
||||
'content' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$dateObj = \Carbon\Carbon::parse($request->date);
|
||||
|
||||
// 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 entries
|
||||
if ($dateObj->isWeekend()) {
|
||||
return response()->json(['success' => false, 'message' => 'Hafta sonu günlerine staj günlüğü girilemez.'], 422);
|
||||
}
|
||||
|
||||
// 3. 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,
|
||||
], [
|
||||
'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.' . ($isRetroactive ? ' (Geriye Dönük Kayıt)' : ''),
|
||||
'entry' => $entry
|
||||
]);
|
||||
}
|
||||
|
||||
public function printJournal(Request $request)
|
||||
{
|
||||
if (request()->has('intern_id') && auth()->check() && auth()->user()->hasRole('super_admin')) {
|
||||
$internId = request('intern_id');
|
||||
} else {
|
||||
if (!session()->has('intern_id')) {
|
||||
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
|
||||
}
|
||||
$internId = session('intern_id');
|
||||
}
|
||||
|
||||
$intern = CareerApplication::findOrFail($internId);
|
||||
$size = $request->query('size', 'a4');
|
||||
if (!in_array($size, ['a4', 'a5'])) {
|
||||
$size = 'a4';
|
||||
}
|
||||
|
||||
$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', [
|
||||
'intern' => $intern,
|
||||
'days' => $days,
|
||||
'savedEntries' => $savedEntries,
|
||||
'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,
|
||||
'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,
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
// 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 session'da locale yoksa, varsayılan dil kodunu kullan
|
||||
// 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 {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InternJournalTimeline extends Component
|
||||
{
|
||||
public ?Model $record = null;
|
||||
|
||||
public function getGithubRepoProperty(): ?string
|
||||
{
|
||||
return $this->record?->github_repo;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.intern-journal-timeline');
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,39 @@ class CareerApplication extends Model
|
||||
'internship_end_date',
|
||||
'internship_total_days',
|
||||
'github_repo',
|
||||
'github_username',
|
||||
'certificate_code',
|
||||
'transcript_markdown',
|
||||
'notebook_supervisor_signed',
|
||||
'notebook_supervisor_name',
|
||||
'notebook_unit_signed',
|
||||
'notebook_unit_name',
|
||||
'notebook_approved',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the journal entries for the internship application.
|
||||
*/
|
||||
public function journalEntries()
|
||||
{
|
||||
return $this->hasMany(InternshipJournalEntry::class);
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::saving(function ($model) {
|
||||
if ($model->type === 'internship') {
|
||||
$model->username = $model->email;
|
||||
if (!$model->certificate_code) {
|
||||
do {
|
||||
$code = 'TRN-' . date('Y') . '-' . strtoupper(\Illuminate\Support\Str::random(4)) . '-' . strtoupper(\Illuminate\Support\Str::random(4));
|
||||
} while (static::where('certificate_code', $code)->exists());
|
||||
$model->certificate_code = $code;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
@@ -41,6 +72,9 @@ class CareerApplication extends Model
|
||||
{
|
||||
return [
|
||||
'password' => 'hashed',
|
||||
'notebook_supervisor_signed' => 'boolean',
|
||||
'notebook_unit_signed' => 'boolean',
|
||||
'notebook_approved' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InternshipJournalEntry extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'career_application_id',
|
||||
'day_number',
|
||||
'date',
|
||||
'content',
|
||||
'is_retroactive',
|
||||
'supervisor_approved',
|
||||
'unit_approved',
|
||||
'supervisor_name',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_retroactive' => 'boolean',
|
||||
'supervisor_approved' => 'boolean',
|
||||
'unit_approved' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the career application that owns this journal entry.
|
||||
*/
|
||||
public function careerApplication()
|
||||
{
|
||||
return $this->belongsTo(CareerApplication::class);
|
||||
}
|
||||
}
|
||||
@@ -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')),
|
||||
]);
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+29
@@ -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('career_applications', function (Blueprint $table) {
|
||||
$table->string('certificate_code')->nullable()->unique();
|
||||
$table->text('transcript_markdown')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('career_applications', function (Blueprint $table) {
|
||||
$table->dropColumn(['certificate_code', 'transcript_markdown']);
|
||||
});
|
||||
}
|
||||
};
|
||||
+29
@@ -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,36 @@
|
||||
<?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('internship_journal_entries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('career_application_id')
|
||||
->constrained('career_applications')
|
||||
->onDelete('cascade');
|
||||
$table->integer('day_number');
|
||||
$table->date('date');
|
||||
$table->text('content')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['career_application_id', 'day_number'], 'journal_app_day_unique');
|
||||
$table->unique(['career_application_id', 'date'], 'journal_app_date_unique');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('internship_journal_entries');
|
||||
}
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?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->boolean('notebook_supervisor_signed')->default(false);
|
||||
$table->string('notebook_supervisor_name')->nullable();
|
||||
$table->boolean('notebook_unit_signed')->default(false);
|
||||
$table->string('notebook_unit_name')->nullable();
|
||||
$table->boolean('notebook_approved')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('career_applications', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'notebook_supervisor_signed',
|
||||
'notebook_supervisor_name',
|
||||
'notebook_unit_signed',
|
||||
'notebook_unit_name',
|
||||
'notebook_approved'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
+28
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
+28
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
+28
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -84,7 +84,7 @@ return [
|
||||
'reviewed' => 'İncelendi',
|
||||
'rejected' => 'Reddedildi',
|
||||
'accepted' => 'Kabul Edildi',
|
||||
'waiting_document' => 'Belge Bekleniyor',
|
||||
'waiting_document' => 'İmzalı Staj Formu Bekleniyor',
|
||||
|
||||
// Wizard and Mermaid
|
||||
'step_1_title' => '1. Şartlar & Koşullar',
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
@import url(https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:ital,wght@1,300;1,400;1,500;1,600;1,700);
|
||||
@import url(https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:ital,wght@1,300;1,400;1,500;1,600;1,700&display=swap);
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url(../../fonts/space/SpaceGrotesk-SemiBold.woff2) format('woff2'), url(../../fonts/space/SpaceGrotesk-SemiBold.woff) format('woff');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url(../../fonts/space/SpaceGrotesk-Light.woff2) format('woff2'), url(../../fonts/space/SpaceGrotesk-Light.woff) format('woff');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url(../../fonts/space/SpaceGrotesk-Bold.woff2) format('woff2'), url(../../fonts/space/SpaceGrotesk-Bold.woff) format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url(../../fonts/space/SpaceGrotesk-Medium.woff2) format('woff2'), url(../../fonts/space/SpaceGrotesk-Medium.woff) format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url(../../fonts/space/SpaceGrotesk-Regular.woff2) format('woff2'), url(../../fonts/space/SpaceGrotesk-Regular.woff) format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
* {
|
||||
word-spacing: normal !important
|
||||
|
||||
@@ -3,70 +3,70 @@
|
||||
src: url(../../fonts/urbanist/Urbanist-BoldItalic.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-BoldItalic.woff) format('woff');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Urbanist;
|
||||
src: url(../../fonts/urbanist/Urbanist-SemiBoldItalic.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-SemiBoldItalic.woff) format('woff');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Urbanist;
|
||||
src: url(../../fonts/urbanist/Urbanist-Medium.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-Medium.woff) format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Urbanist;
|
||||
src: url(../../fonts/urbanist/Urbanist-MediumItalic.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-MediumItalic.woff) format('woff');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Urbanist;
|
||||
src: url(../../fonts/urbanist/Urbanist-SemiBold.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-SemiBold.woff) format('woff');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Urbanist;
|
||||
src: url(../../fonts/urbanist/Urbanist-Italic.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-Italic.woff) format('woff');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Urbanist;
|
||||
src: url(../../fonts/urbanist/Urbanist-Regular.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-Regular.woff) format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Urbanist;
|
||||
src: url(../../fonts/urbanist/Urbanist-LightItalic.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-LightItalic.woff) format('woff');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Urbanist;
|
||||
src: url(../../fonts/urbanist/Urbanist-Light.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-Light.woff) format('woff');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Urbanist;
|
||||
src: url(../../fonts/urbanist/Urbanist-Bold.woff2) format('woff2'), url(../../fonts/urbanist/Urbanist-Bold.woff) format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: block
|
||||
font-display: swap;
|
||||
}
|
||||
* {
|
||||
word-spacing: normal !important
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
src:url(Unicons.woff2) format("woff2"),url(Unicons.woff) format("woff");
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
font-display:block
|
||||
font-display:swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family:Custom;
|
||||
src:url(../custom/Custom.woff2) format("woff2"),url(../custom/Custom.woff) format("woff");
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
font-display:block
|
||||
font-display:swap;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 740 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 816 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 752 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 704 KiB |
@@ -0,0 +1 @@
|
||||
google-site-verification: googlef1M6GQLxVA5g7IaVY6kMQCoIrgADR6HjrOZu79CrXYc.html
|
||||
+1
-31862
File diff suppressed because one or more lines are too long
+5
-1
@@ -1,2 +1,6 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Allow: /
|
||||
Disallow: /admin/
|
||||
Disallow: /stajyer/admin/
|
||||
|
||||
Sitemap: https://truncgil.com/sitemap.xml
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
{{-- Bu bileşen görünümü devre dışı bırakılmıştır. --}}
|
||||
@@ -1,86 +1,50 @@
|
||||
@php
|
||||
$githubRepo = $get('github_repo') ?? ($record ? $record->github_repo : null);
|
||||
$uid = 'journal_' . uniqid();
|
||||
$githubRepo = $get('github_repo') ?? ($getRecord ? $getRecord()->github_repo : null);
|
||||
@endphp
|
||||
|
||||
<div id="{{ $uid }}" class="mt-4">
|
||||
<!-- Loader -->
|
||||
<div id="{{ $uid }}-loader" class="hidden py-8 flex flex-col items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mb-2"></div>
|
||||
<span class="text-xs text-gray-500 font-semibold">GitHub'dan commitler yükleniyor...</span>
|
||||
</div>
|
||||
<div
|
||||
x-data="{
|
||||
githubRepoUrl: @js($githubRepo),
|
||||
commitDays: [],
|
||||
activeDayIndex: 0,
|
||||
loading: false,
|
||||
errorMsg: '',
|
||||
isEmpty: false,
|
||||
|
||||
<!-- Error Alert -->
|
||||
<div id="{{ $uid }}-error" class="hidden p-4 rounded-xl bg-danger-50 border border-danger-200 text-danger-600 text-xs font-semibold">
|
||||
</div>
|
||||
init() {
|
||||
this.loadCommits();
|
||||
},
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="{{ $uid }}-empty" class="hidden p-6 rounded-2xl border border-gray-150 text-center text-gray-400 italic text-sm">
|
||||
Henüz hiç commit bulunamadı veya stajyerin deponuz tanımlanmadı.
|
||||
</div>
|
||||
escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(text));
|
||||
return div.innerHTML;
|
||||
},
|
||||
|
||||
<!-- Day Paginator / Navbar -->
|
||||
<div id="{{ $uid }}-carousel-nav" class="hidden flex items-center justify-between gap-4 p-3 bg-gray-50 border border-gray-100 rounded-2xl mb-6 select-none">
|
||||
<button type="button" id="{{ $uid }}-prev-day-btn" onclick="{{ $uid }}_navigateDay(-1)" class="w-10 h-10 rounded-xl bg-white hover:bg-gray-100 text-gray-600 border border-gray-200 flex items-center justify-center transition-all shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="flex-grow overflow-x-auto no-scrollbar py-1">
|
||||
<div id="{{ $uid }}-tabs-container" class="flex gap-2 justify-start sm:justify-center min-w-max px-2">
|
||||
<!-- Day tabs rendered dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="{{ $uid }}-next-day-btn" onclick="{{ $uid }}_navigateDay(1)" class="w-10 h-10 rounded-xl bg-white hover:bg-gray-100 text-gray-600 border border-gray-200 flex items-center justify-center transition-all shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Run on init script -->
|
||||
<script>
|
||||
(function() {
|
||||
const uid = '{{ $uid }}';
|
||||
const githubRepoUrl = @json($githubRepo);
|
||||
let commitDays = [];
|
||||
let activeDayIndex = 0;
|
||||
|
||||
window[uid + '_loadGithubCommits'] = function() {
|
||||
const loader = document.getElementById(uid + '-loader');
|
||||
const errorEl = document.getElementById(uid + '-error');
|
||||
const emptyEl = document.getElementById(uid + '-empty');
|
||||
const navEl = document.getElementById(uid + '-carousel-nav');
|
||||
const containerEl = document.getElementById(uid + '-timeline-container');
|
||||
|
||||
if (!githubRepoUrl) {
|
||||
emptyEl.classList.remove('hidden');
|
||||
loadCommits() {
|
||||
if (!this.githubRepoUrl) {
|
||||
this.isEmpty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
loader.classList.remove('hidden');
|
||||
errorEl.classList.add('hidden');
|
||||
emptyEl.classList.add('hidden');
|
||||
navEl.classList.add('hidden');
|
||||
containerEl.classList.add('hidden');
|
||||
commitDays = [];
|
||||
this.loading = true;
|
||||
this.errorMsg = '';
|
||||
this.isEmpty = false;
|
||||
this.commitDays = [];
|
||||
|
||||
// Parse owner and repo
|
||||
let repoClean = githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
|
||||
let repoClean = this.githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
|
||||
repoClean = repoClean.replace(/\/$/, '');
|
||||
const parts = repoClean.split('/');
|
||||
if (parts.length < 2) {
|
||||
loader.classList.add('hidden');
|
||||
errorEl.textContent = 'Github depo URL\'si çözümlenemedi. Geçerli format: https://github.com/kullanici/depo';
|
||||
errorEl.classList.remove('hidden');
|
||||
this.loading = false;
|
||||
this.errorMsg = 'Github depo URL\'si çözümlenemedi. Geçerli format: https://github.com/kullanici/depo';
|
||||
return;
|
||||
}
|
||||
const owner = parts[0];
|
||||
const repo = parts[1].replace(/\.git$/i, '');
|
||||
|
||||
fetch(`https://api.github.com/repos/${owner}/${repo}/commits?per_page=100`)
|
||||
fetch('https://api.github.com/repos/' + owner + '/' + repo + '/commits?per_page=100')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
@@ -94,16 +58,14 @@
|
||||
return response.json();
|
||||
})
|
||||
.then(commits => {
|
||||
loader.classList.add('hidden');
|
||||
this.loading = false;
|
||||
if (!Array.isArray(commits) || commits.length === 0) {
|
||||
emptyEl.classList.remove('hidden');
|
||||
this.isEmpty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Reverse to show in chronological order
|
||||
commits.reverse();
|
||||
|
||||
// Group commits by date (YYYY-MM-DD)
|
||||
const grouped = {};
|
||||
commits.forEach(item => {
|
||||
const dateStr = item.commit.author.date;
|
||||
@@ -118,171 +80,204 @@
|
||||
|
||||
const sortedDates = Object.keys(grouped).sort();
|
||||
|
||||
commitDays = sortedDates.map((date, index) => {
|
||||
this.commitDays = sortedDates.map((date, index) => {
|
||||
const dateParts = date.split('-');
|
||||
return {
|
||||
date: date,
|
||||
dayNum: index + 1,
|
||||
formattedDate: `${dateParts[2]}.${dateParts[1]}.${dateParts[0]}`,
|
||||
commits: grouped[date]
|
||||
formattedDate: dateParts[2] + '.' + dateParts[1] + '.' + dateParts[0],
|
||||
commits: grouped[date].map(c => ({
|
||||
message: c.commit.message,
|
||||
time: new Date(c.commit.author.date).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' }),
|
||||
sha: c.sha.substring(0, 7),
|
||||
url: c.html_url
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
if (commitDays.length === 0) {
|
||||
emptyEl.classList.remove('hidden');
|
||||
if (this.commitDays.length === 0) {
|
||||
this.isEmpty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
activeDayIndex = 0;
|
||||
navEl.classList.remove('hidden');
|
||||
containerEl.classList.remove('hidden');
|
||||
|
||||
renderDayTabs();
|
||||
renderActiveDay();
|
||||
this.activeDayIndex = 0;
|
||||
})
|
||||
.catch(err => {
|
||||
loader.classList.add('hidden');
|
||||
errorEl.textContent = err.message || 'Bir hata oluştu.';
|
||||
errorEl.classList.remove('hidden');
|
||||
this.loading = false;
|
||||
this.errorMsg = err.message || 'Bir hata oluştu.';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
function renderDayTabs() {
|
||||
const container = document.getElementById(uid + '-tabs-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
commitDays.forEach((day, index) => {
|
||||
const isActive = index === activeDayIndex;
|
||||
const activeClass = isActive
|
||||
? 'text-primary-700 bg-primary-50 border border-primary-200 font-extrabold shadow-sm'
|
||||
: 'bg-white hover:bg-gray-50 text-gray-600 border border-gray-200 font-semibold hover:text-primary-600';
|
||||
|
||||
const tab = document.createElement('button');
|
||||
tab.type = 'button';
|
||||
tab.className = `px-5 py-2 rounded-xl transition-all flex flex-col items-center justify-center ${activeClass}`;
|
||||
tab.innerHTML = `
|
||||
<span class="text-xs">${day.dayNum}. Gün</span>
|
||||
<span class="text-[9px] mt-0.5 opacity-70 font-medium">${day.formattedDate}</span>
|
||||
`;
|
||||
tab.onclick = () => {
|
||||
activeDayIndex = index;
|
||||
renderDayTabs();
|
||||
renderActiveDay();
|
||||
};
|
||||
container.appendChild(tab);
|
||||
});
|
||||
get activeDay() {
|
||||
return this.commitDays[this.activeDayIndex] || null;
|
||||
},
|
||||
|
||||
const activeTab = container.children[activeDayIndex];
|
||||
if (activeTab) {
|
||||
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||||
navigateDay(dir) {
|
||||
const newIndex = this.activeDayIndex + dir;
|
||||
if (newIndex >= 0 && newIndex < this.commitDays.length) {
|
||||
this.activeDayIndex = newIndex;
|
||||
}
|
||||
},
|
||||
|
||||
const prevBtn = document.getElementById(uid + '-prev-day-btn');
|
||||
const nextBtn = document.getElementById(uid + '-next-day-btn');
|
||||
|
||||
prevBtn.disabled = activeDayIndex === 0;
|
||||
prevBtn.classList.toggle('opacity-40', activeDayIndex === 0);
|
||||
prevBtn.classList.toggle('cursor-not-allowed', activeDayIndex === 0);
|
||||
|
||||
nextBtn.disabled = activeDayIndex === commitDays.length - 1;
|
||||
nextBtn.classList.toggle('opacity-40', activeDayIndex === commitDays.length - 1);
|
||||
nextBtn.classList.toggle('cursor-not-allowed', activeDayIndex === commitDays.length - 1);
|
||||
selectDay(index) {
|
||||
this.activeDayIndex = index;
|
||||
}
|
||||
|
||||
function renderActiveDay() {
|
||||
const card = document.getElementById(uid + '-active-day-card');
|
||||
|
||||
card.style.opacity = '0.3';
|
||||
card.style.transform = 'translateY(5px)';
|
||||
|
||||
setTimeout(() => {
|
||||
const day = commitDays[activeDayIndex];
|
||||
|
||||
document.getElementById(uid + '-active-day-title').textContent = `${day.dayNum}. Gün`;
|
||||
document.getElementById(uid + '-active-day-date').innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
||||
</svg>
|
||||
<span>${day.formattedDate}</span>
|
||||
`;
|
||||
document.getElementById(uid + '-active-day-commit-count').textContent = `${day.commits.length} Commit`;
|
||||
|
||||
const eventsContainer = document.getElementById(uid + '-timeline-events');
|
||||
eventsContainer.innerHTML = '';
|
||||
|
||||
day.commits.forEach(c => {
|
||||
const message = c.commit.message;
|
||||
const authorDate = new Date(c.commit.author.date);
|
||||
const timeStr = authorDate.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
|
||||
const shaShort = c.sha.substring(0, 7);
|
||||
const commitUrl = c.html_url;
|
||||
|
||||
const eventHtml = `
|
||||
<div class="relative group">
|
||||
<!-- Timeline Dot -->
|
||||
<div class="absolute -left-[31px] sm:-left-[39px] top-1.5 w-6 h-6 rounded-full bg-white border-2 border-primary-500 flex items-center justify-center transition-all group-hover:bg-primary-500">
|
||||
<div class="w-2 h-2 rounded-full bg-primary-500 group-hover:bg-white transition-all"></div>
|
||||
</div>
|
||||
<!-- Event Card -->
|
||||
<div class="p-4 bg-gray-50/50 hover:bg-primary-50/10 border border-gray-150 hover:border-primary-100 rounded-2xl transition-all shadow-sm">
|
||||
<div class="flex items-center justify-between gap-4 mb-2">
|
||||
<span class="text-xs font-bold text-gray-450 flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>${timeStr}</span>
|
||||
</span>
|
||||
<a href="${commitUrl}" target="_blank" class="text-xs font-mono font-bold text-primary-600 hover:text-primary-700 bg-primary-50 hover:bg-primary-100 px-2.5 py-0.5 rounded-lg border border-primary-100 transition-colors">
|
||||
${shaShort}
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-sm font-bold text-gray-750 leading-relaxed whitespace-pre-line">${escapeHtml(message)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
eventsContainer.insertAdjacentHTML('beforeend', eventHtml);
|
||||
});
|
||||
|
||||
card.style.opacity = '1';
|
||||
card.style.transform = 'translateY(0)';
|
||||
}, 150);
|
||||
}
|
||||
|
||||
window[uid + '_navigateDay'] = function(dir) {
|
||||
const newIndex = activeDayIndex + dir;
|
||||
if (newIndex >= 0 && newIndex < commitDays.length) {
|
||||
activeDayIndex = newIndex;
|
||||
renderDayTabs();
|
||||
renderActiveDay();
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// Run on init
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', window[uid + '_loadGithubCommits']);
|
||||
} else {
|
||||
window[uid + '_loadGithubCommits']();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
}"
|
||||
style="margin-top: 1rem; font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;"
|
||||
>
|
||||
<style>
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.jt-day-btn {
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
.jt-day-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
.jt-arrow-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.jt-arrow-btn:hover:not(:disabled) {
|
||||
transform: scale(1.08);
|
||||
background: #eff6ff !important;
|
||||
border-color: #93c5fd !important;
|
||||
}
|
||||
.jt-arrow-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.jt-event-card {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.jt-event-card:hover {
|
||||
transform: translateY(-2px) translateX(3px);
|
||||
box-shadow: 0 12px 24px -8px rgba(59, 130, 246, 0.12);
|
||||
border-color: #bfdbfe !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
.jt-timeline-dot {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.jt-event-row:hover .jt-timeline-dot {
|
||||
transform: scale(1.2);
|
||||
background-color: #3b82f6 !important;
|
||||
box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.18);
|
||||
}
|
||||
.jt-event-row:hover .jt-timeline-dot-inner {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
.jt-sha-link {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
.jt-sha-link:hover {
|
||||
background: #3b82f6 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
.jt-paginator-scroll::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
.jt-paginator-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.jt-paginator-scroll::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.jt-paginator-scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 transparent;
|
||||
}
|
||||
@keyframes jt-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes jt-fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.jt-fade-in {
|
||||
animation: jt-fadeIn 0.4s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{-- Loader --}}
|
||||
<div x-show="loading" x-cloak style="padding: 3rem 0; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div style="width: 2.5rem; height: 2.5rem; border: 2px solid #e2e8f0; border-top-color: #3b82f6; border-radius: 50%; animation: jt-spin 0.8s linear infinite; margin-bottom: 0.75rem;"></div>
|
||||
<span style="font-size: 0.75rem; color: #94a3b8; font-weight: 600; letter-spacing: 0.025em;">Commit geçmişi yükleniyor...</span>
|
||||
</div>
|
||||
|
||||
{{-- Error --}}
|
||||
<div x-show="errorMsg" x-cloak x-text="errorMsg" style="padding: 1rem 1.25rem; border-radius: 1rem; background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; font-size: 0.75rem; font-weight: 700;"></div>
|
||||
|
||||
{{-- Empty --}}
|
||||
<div x-show="isEmpty && !loading" x-cloak style="padding: 2rem; border-radius: 1.5rem; border: 1px solid #f1f5f9; background: #ffffff; text-align: center; color: #94a3b8; font-style: italic; font-size: 0.875rem;">
|
||||
Henüz hiçbir commit verisi bulunamadı veya depo adresi yapılandırılmadı.
|
||||
</div>
|
||||
|
||||
{{-- Day Paginator --}}
|
||||
<div x-show="commitDays.length > 0" x-cloak style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.75rem; background: rgba(255,255,255,0.8); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid #e2e8f0; border-radius: 1.5rem; margin-bottom: 1.75rem; user-select: none; box-shadow: 0 1px 3px rgba(0,0,0,0.04);">
|
||||
<button type="button" @click="navigateDay(-1)" :disabled="activeDayIndex === 0" class="jt-arrow-btn" style="width: 2.75rem; height: 2.75rem; border-radius: 0.875rem; background: #ffffff; color: #64748b; border: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; box-shadow: 0 1px 2px rgba(0,0,0,0.04);">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="jt-paginator-scroll" style="flex: 1; overflow-x: auto; padding: 0.375rem 0;">
|
||||
<div style="display: flex; gap: 0.625rem; justify-content: center; min-width: max-content; padding: 0 0.5rem;">
|
||||
<template x-for="(day, index) in commitDays" :key="day.date">
|
||||
<button type="button" @click="selectDay(index)" class="jt-day-btn" :style="index === activeDayIndex
|
||||
? 'padding: 0.5rem 1.25rem; border-radius: 0.875rem; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 1.5px solid #93c5fd; background: linear-gradient(135deg, #eff6ff, #dbeafe); color: #1d4ed8; font-weight: 800; box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);'
|
||||
: 'padding: 0.5rem 1.25rem; border-radius: 0.875rem; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 1px solid #e2e8f0; background: #ffffff; color: #64748b; font-weight: 600;'">
|
||||
<span style="font-size: 0.75rem; line-height: 1.2;" x-text="day.dayNum + '. Gün'"></span>
|
||||
<span style="font-size: 0.6rem; margin-top: 0.125rem; opacity: 0.7; font-weight: 500;" x-text="day.formattedDate"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" @click="navigateDay(1)" :disabled="activeDayIndex === commitDays.length - 1" class="jt-arrow-btn" style="width: 2.75rem; height: 2.75rem; border-radius: 0.875rem; background: #ffffff; color: #64748b; border: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; box-shadow: 0 1px 2px rgba(0,0,0,0.04);">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Active Day Card --}}
|
||||
<div x-show="activeDay" x-cloak class="jt-fade-in" :key="activeDayIndex" style="background: #ffffff; border: 1px solid #f1f5f9; border-radius: 1.5rem; padding: 1.75rem 2rem; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.06);">
|
||||
{{-- Day Header --}}
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.75rem; padding-bottom: 1.25rem; border-bottom: 1px solid #f1f5f9;">
|
||||
<div style="display: flex; align-items: center; gap: 0.875rem;">
|
||||
<span style="font-size: 0.875rem; font-weight: 800; color: #2563eb; background: linear-gradient(135deg, #eff6ff, #dbeafe); padding: 0.5rem 1.125rem; border-radius: 1rem; box-shadow: 0 1px 3px rgba(37, 99, 235, 0.1);" x-text="activeDay ? activeDay.dayNum + '. Gün' : ''"></span>
|
||||
<span style="font-size: 0.8125rem; font-weight: 600; color: #94a3b8; display: flex; align-items: center; gap: 0.375rem;">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: #60a5fa;"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||
<span x-text="activeDay ? activeDay.formattedDate : ''"></span>
|
||||
</span>
|
||||
</div>
|
||||
<span style="font-size: 0.6875rem; font-weight: 700; color: #94a3b8; background: #f8fafc; border: 1px solid #f1f5f9; padding: 0.375rem 0.875rem; border-radius: 0.625rem;" x-text="activeDay ? activeDay.commits.length + ' Commit' : ''"></span>
|
||||
</div>
|
||||
|
||||
{{-- Timeline --}}
|
||||
<div style="position: relative; padding-left: 2.25rem;">
|
||||
{{-- Gradient Timeline Line --}}
|
||||
<div style="position: absolute; left: 11px; top: 8px; bottom: 8px; width: 2px; background: linear-gradient(180deg, #3b82f6 0%, #818cf8 40%, #c7d2fe 100%); border-radius: 2px;"></div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 1.25rem;">
|
||||
<template x-for="(commit, ci) in (activeDay ? activeDay.commits : [])" :key="ci">
|
||||
<div class="jt-event-row" style="position: relative;">
|
||||
{{-- Timeline Dot --}}
|
||||
<div class="jt-timeline-dot" style="position: absolute; left: -32px; top: 6px; width: 24px; height: 24px; border-radius: 50%; background: #ffffff; border: 2.5px solid #3b82f6; display: flex; align-items: center; justify-content: center; z-index: 1;">
|
||||
<div class="jt-timeline-dot-inner" style="width: 8px; height: 8px; border-radius: 50%; background: #3b82f6;"></div>
|
||||
</div>
|
||||
{{-- Event Card --}}
|
||||
<div class="jt-event-card" style="padding: 1.125rem 1.25rem; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.03);">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 0.625rem;">
|
||||
<span style="font-size: 0.75rem; font-weight: 700; color: #94a3b8; display: flex; align-items: center; gap: 0.375rem;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#60a5fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span x-text="commit.time"></span>
|
||||
</span>
|
||||
<a :href="commit.url" target="_blank" class="jt-sha-link" x-text="commit.sha" style="font-size: 0.6875rem; font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 700; color: #2563eb; background: #eff6ff; padding: 0.25rem 0.75rem; border-radius: 0.5rem; border: 1px solid #bfdbfe; text-decoration: none;"></a>
|
||||
</div>
|
||||
<p style="font-size: 0.8125rem; font-weight: 600; color: #334155; line-height: 1.6; white-space: pre-line; margin: 0;" x-text="commit.message"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
@extends('layouts.site')
|
||||
|
||||
@section('content')
|
||||
<section class="wrapper bg-[#f0f7ff] py-12">
|
||||
<div class="container px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
|
||||
<!-- Welcome Section -->
|
||||
@include('front.career.partials.welcome')
|
||||
|
||||
<!-- Statistics Section -->
|
||||
@include('front.career.partials.stats')
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
</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-end flex-shrink-0">
|
||||
<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;
|
||||
}
|
||||
</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 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,87 @@
|
||||
@extends('layouts.site')
|
||||
|
||||
@section('content')
|
||||
<section class="wrapper bg-[#f0f7ff] min-h-[70vh] flex items-center py-12">
|
||||
<div class="container px-4">
|
||||
<div class="max-w-md mx-auto bg-white/80 backdrop-blur-md rounded-3xl shadow-2xl border border-slate-100/50 overflow-hidden">
|
||||
<div class="p-8 md:p-10">
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex w-16 h-16 rounded-full bg-red-50 text-[#e31e24] items-center justify-center mb-4">
|
||||
<i class="uil uil-shield text-3xl"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl font-extrabold text-slate-900 tracking-tight">Yönetici Girişi</h1>
|
||||
<p class="text-sm text-slate-500 mt-2">Staj takip sistemini yönetmek için e-posta ve şifrenizle giriş yapın.</p>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-50 border-l-4 border-[#e31e24] p-4 rounded-xl mb-6">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="uil uil-exclamation-triangle text-[#e31e24] text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-red-700 font-medium">
|
||||
{{ $errors->first() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('success'))
|
||||
<div class="bg-green-50 border-l-4 border-green-500 p-4 rounded-xl mb-6">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="uil uil-check-circle text-green-500 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-green-700 font-medium">
|
||||
{{ session('success') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('intern.admin.login.submit') }}" method="POST" class="space-y-6">
|
||||
@csrf
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="email" class="text-sm font-bold text-slate-700 block">E-posta</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 text-slate-400">
|
||||
<i class="uil uil-envelope"></i>
|
||||
</span>
|
||||
<input type="email" name="email" id="email" class="w-full pl-11 pr-5 py-3.5 rounded-xl border border-slate-200 focus:ring-4 focus:ring-red-500/10 focus:border-[#e31e24] outline-none transition-all placeholder-slate-300" placeholder="E-posta adresinizi girin" value="{{ old('email') }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="password" class="text-sm font-bold text-slate-700 block">Şifre</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 text-slate-400">
|
||||
<i class="uil uil-key-skeleton-alt"></i>
|
||||
</span>
|
||||
<input type="password" name="password" id="password" class="w-full pl-11 pr-5 py-3.5 rounded-xl border border-slate-200 focus:ring-4 focus:ring-red-500/10 focus:border-[#e31e24] outline-none transition-all placeholder-slate-300" placeholder="Şifrenizi girin" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full py-4 bg-[#e31e24] hover:bg-[#c4191f] text-white font-bold rounded-xl transition-all shadow-lg shadow-red-500/20 flex items-center justify-center gap-2 mt-4 hover:-translate-y-0.5">
|
||||
<span>Giriş Yap</span>
|
||||
<i class="uil uil-arrow-right text-xl"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@push('styles')
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
.shadow-2xl {
|
||||
box-shadow: 0 1.5rem 4rem rgba(30, 41, 59, 0.08) !important;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,338 @@
|
||||
<!-- Gantt Chart Card -->
|
||||
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50 mb-8">
|
||||
<div class="flex items-center justify-between mb-6 pb-4 border-b border-slate-100">
|
||||
<h3 class="font-bold text-slate-800 text-lg flex items-center gap-2">
|
||||
<i class="uil uil-schedule text-blue-600 text-xl"></i>
|
||||
<span>Stajyer Gantt Şeması ve Takvimi</span>
|
||||
</h3>
|
||||
<span class="text-xs px-2.5 py-1 bg-blue-50 text-blue-600 rounded-full font-semibold">DevExtreme Görünümü</span>
|
||||
</div>
|
||||
|
||||
@if($ganttInterns->isEmpty())
|
||||
<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-calendar-slash text-4xl text-slate-300 mb-2"></i>
|
||||
<p class="text-sm font-medium text-slate-500">Tarihleri belirlenmiş onaylı stajyer bulunamadı.</p>
|
||||
</div>
|
||||
@else
|
||||
<!-- Gantt Container -->
|
||||
<div class="dx-viewport demo-container" style="height: 480px; overflow: hidden; border-radius: 16px; border: 1px solid rgba(0,0,0,0.05);">
|
||||
<div id="gantt" style="height: 100%; width: 100%;"></div>
|
||||
</div>
|
||||
@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">
|
||||
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.5/css/dx-gantt.min.css">
|
||||
<style>
|
||||
.current-time-line {
|
||||
background-color: rgba(239, 68, 68, 0.4) !important;
|
||||
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 = {
|
||||
config: {
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<!-- Gantt Chart Javascript Dependencies & Initialization -->
|
||||
@if(!$ganttInterns->isEmpty())
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn3.devexpress.com/jslib/23.2.5/js/dx-gantt.min.js"></script>
|
||||
<script src="https://cdn3.devexpress.com/jslib/23.2.5/js/dx.all.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
const ganttData = @json($ganttInterns);
|
||||
|
||||
const formattedData = ganttData.map(intern => ({
|
||||
id: intern.id,
|
||||
title: intern.title,
|
||||
start: new Date(intern.start),
|
||||
end: new Date(intern.end),
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
let dayColumnsMap = {};
|
||||
|
||||
$('#gantt').dxGantt({
|
||||
tasks: {
|
||||
dataSource: formattedData,
|
||||
},
|
||||
editing: {
|
||||
enabled: false,
|
||||
},
|
||||
validation: {
|
||||
autoUpdateParentTasks: true,
|
||||
},
|
||||
toolbar: {
|
||||
items: [
|
||||
'collapseAll',
|
||||
'expandAll',
|
||||
'separator',
|
||||
'zoomIn',
|
||||
'zoomOut',
|
||||
],
|
||||
},
|
||||
columns: [{
|
||||
dataField: 'title',
|
||||
caption: 'Stajyer Adı',
|
||||
width: 200,
|
||||
}, {
|
||||
dataField: 'start',
|
||||
caption: 'Başlangıç Tarihi',
|
||||
dataType: 'date',
|
||||
format: 'dd.MM.yyyy',
|
||||
width: 110,
|
||||
}, {
|
||||
dataField: 'end',
|
||||
caption: 'Bitiş Tarihi',
|
||||
dataType: 'date',
|
||||
format: 'dd.MM.yyyy',
|
||||
width: 110,
|
||||
}],
|
||||
scaleType: 'days',
|
||||
taskListWidth: 420,
|
||||
stripLines: [{
|
||||
title: 'Bugün',
|
||||
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
|
||||
@@ -0,0 +1,132 @@
|
||||
<!-- Stajyer Rehberi Modal -->
|
||||
<div id="guideModal" class="fixed inset-0 z-[9999] hidden flex items-center justify-center p-4">
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-md transition-opacity duration-300" onclick="toggleGuideModal(false)"></div>
|
||||
|
||||
<!-- Modal Content Card -->
|
||||
<div class="relative bg-white rounded-3xl w-full max-w-2xl max-h-[85vh] overflow-y-auto shadow-2xl border border-slate-100/50 transform scale-95 opacity-0 transition-all duration-300 flex flex-col" id="guideModalCard">
|
||||
|
||||
<!-- Modal Header -->
|
||||
<div class="sticky top-0 bg-white z-10 px-6 py-5 border-b border-slate-100 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-red-50 text-[#e31e24] flex items-center justify-center text-xl">
|
||||
<i class="uil uil-book-open"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-slate-800 text-lg text-white">Stajyer Çalışma & Git Workflow Rehberi</h3>
|
||||
<p class="text-xs text-slate-400 font-semibold mt-0.5">Başarılı bir staj süreci için uymanız gereken kurallar</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="toggleGuideModal(false)" class="w-8 h-8 rounded-lg bg-slate-50 hover:bg-slate-100 text-slate-400 hover:text-slate-600 flex items-center justify-center transition-colors">
|
||||
<i class="uil uil-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="p-6 md:p-8 space-y-6">
|
||||
|
||||
<!-- Bullet 1: GitHub Public Repo -->
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0 mt-1 font-bold text-xs">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-extrabold text-slate-800 text-sm">GitHub Public Repo Oluşturma</h4>
|
||||
<p class="text-xs text-slate-500 mt-1 leading-relaxed">
|
||||
Staj süresince geliştireceğiniz projeler için GitHub üzerinde <strong>public (herkese açık)</strong> bir repository oluşturmanız ve bu repo bağlantısını stajyer panelinize kaydetmeniz gerekmektedir.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bullet 2: Implementation Plan -->
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0 mt-1 font-bold text-xs">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-extrabold text-slate-800 text-sm">İlk Adım: Implementation Plan</h4>
|
||||
<p class="text-xs text-slate-500 mt-1 leading-relaxed">
|
||||
Deponuzu oluşturduktan sonra ilk commit olarak ana dizinde <code>implementation_plan.md</code> adında bir dosya hazırlamalısınız. Bu plan, projenizin mimarisini, hedeflerinizi ve yapacağınız işlerin genel haritasını içermelidir.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bullet 3: Daily Commits & Intern Journal -->
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0 mt-1 font-bold text-xs">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-extrabold text-slate-800 text-sm">Günlük Commit Zorunluluğu (Staj Defteri)</h4>
|
||||
<p class="text-xs text-slate-500 mt-1 leading-relaxed">
|
||||
Projeyi geliştirirken yaptığınız günlük commitler otomatik olarak staj günlüğünüze işlenir ve staj defterinizin içeriğini oluşturur. <strong>Eğer o gün commit gönderilmemişse, o gün çalışılmamış sayılacaktır.</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bullet 4: Daily Standup Meetings -->
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0 mt-1 font-bold text-xs">
|
||||
4
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-extrabold text-slate-800 text-sm">Daily Standup Toplantıları</h4>
|
||||
<p class="text-xs text-slate-500 mt-1 leading-relaxed">
|
||||
Her iş günü düzenli olarak daily standup (günlük durum değerlendirme) toplantısı yapılacaktır. Toplantılarda dünün özeti, bugünün planları ve karşılaşılan engeller paylaşılır. <strong>Tüm stajyerlerin katılımı zorunludur.</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bullet 5: Git Workflow & Commit Culture -->
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0 mt-1 font-bold text-xs">
|
||||
5
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-extrabold text-slate-800 text-sm">Git Workflow & Temiz Kod Kültürü</h4>
|
||||
<p class="text-xs text-slate-500 mt-1 leading-relaxed">
|
||||
Anlamlı commit mesajları yazmaya özen gösterin (örn. <code>feat: add login validation</code>). Kodlarınızı mantıksal parçalara ayırarak commit atın ve Git versiyon kontrol standartlarına uyun.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="sticky bottom-0 bg-slate-50 z-10 px-6 py-4 border-t border-slate-100/50 flex items-center justify-end gap-3 rounded-b-3xl">
|
||||
<button onclick="toggleGuideModal(false)" class="px-5 py-2.5 bg-slate-850 hover:bg-slate-900 text-white font-extrabold rounded-xl text-xs transition-colors shadow-md shadow-slate-800/10">
|
||||
Anladım, Kapat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleGuideModal(show) {
|
||||
const modal = document.getElementById('guideModal');
|
||||
const card = document.getElementById('guideModalCard');
|
||||
if (!modal || !card) return;
|
||||
|
||||
if (show) {
|
||||
modal.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
card.classList.remove('scale-95', 'opacity-0');
|
||||
card.classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
} else {
|
||||
card.classList.remove('scale-100', 'opacity-100');
|
||||
card.classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ESC key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
toggleGuideModal(false);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,139 @@
|
||||
<!-- Interns Table & Management List -->
|
||||
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50">
|
||||
<h3 class="font-bold text-slate-800 text-lg mb-6 pb-4 border-b border-slate-100 flex items-center gap-2">
|
||||
<i class="uil uil-list-ul text-[#e31e24] text-xl"></i>
|
||||
<span>Stajyer Listesi ve Detaylı Takip</span>
|
||||
</h3>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<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>
|
||||
<th class="pb-4">Kayıt Tarihi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@foreach($allInterns as $intern)
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<!-- Name & Info -->
|
||||
<td class="py-4">
|
||||
<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)
|
||||
<span><i class="uil uil-phone mr-1"></i>{{ $intern->phone }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Internship Dates -->
|
||||
<td class="py-4 text-xs font-semibold text-slate-600">
|
||||
@if($intern->internship_start_date && $intern->internship_end_date)
|
||||
<div class="flex flex-col">
|
||||
<span>{{ \Carbon\Carbon::parse($intern->internship_start_date)->format('d.m.Y') }} - {{ \Carbon\Carbon::parse($intern->internship_end_date)->format('d.m.Y') }}</span>
|
||||
<span class="text-blue-500 font-extrabold mt-0.5">{{ $intern->internship_total_days }} İş Günü</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-slate-300 italic">Belirlenmemiş</span>
|
||||
@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
|
||||
@if($intern->status === 'accepted') bg-green-50 text-green-700 border border-green-200
|
||||
@elseif($intern->status === 'rejected') bg-red-50 text-red-700 border border-red-200
|
||||
@elseif($intern->status === 'reviewed') bg-blue-50 text-blue-700 border border-blue-200
|
||||
@elseif($intern->status === 'waiting_document') bg-amber-50 text-amber-700 border border-amber-200
|
||||
@else bg-slate-50 text-slate-500 border border-slate-200 @endif">
|
||||
@if($intern->status === 'accepted') Kabul Edildi
|
||||
@elseif($intern->status === 'rejected') Reddedildi
|
||||
@elseif($intern->status === 'reviewed') İncelendi
|
||||
@elseif($intern->status === 'waiting_document') Evrak Bekleniyor
|
||||
@else Beklemede @endif
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Documents -->
|
||||
<td class="py-4 space-y-1">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@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>
|
||||
@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>
|
||||
@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>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- GitHub Repository -->
|
||||
<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">
|
||||
<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">
|
||||
<i class="uil uil-arrow-down-tray"></i> Günlüğü İndir (.md)
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-slate-300 italic">Tanımlanmamış</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<!-- Register Date -->
|
||||
<td class="py-4 text-xs font-semibold text-slate-400">
|
||||
{{ \Carbon\Carbon::parse($intern->created_at)->format('d.m.Y H:i') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white p-6 rounded-3xl border border-slate-100 shadow-lg flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-2xl bg-blue-50 text-blue-600 flex items-center justify-center text-2xl">
|
||||
<i class="uil uil-users-alt"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs font-bold text-slate-400 block uppercase">Toplam Stajyer</span>
|
||||
<span class="text-2xl font-extrabold text-slate-800">{{ $allInterns->count() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-3xl border border-slate-100 shadow-lg flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-2xl bg-emerald-50 text-emerald-600 flex items-center justify-center text-2xl">
|
||||
<i class="uil uil-check-circle"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs font-bold text-slate-400 block uppercase">Kabul Edilen</span>
|
||||
<span class="text-2xl font-extrabold text-slate-800">{{ $allInterns->where('status', 'accepted')->count() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-3xl border border-slate-100 shadow-lg flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-2xl bg-amber-50 text-amber-600 flex items-center justify-center text-2xl">
|
||||
<i class="uil uil-hourglass-half"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs font-bold text-slate-400 block uppercase">Evrak Bekleyen</span>
|
||||
<span class="text-2xl font-extrabold text-slate-800">{{ $allInterns->where('status', 'waiting_document')->count() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-3xl border border-slate-100 shadow-lg flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-2xl bg-indigo-50 text-indigo-600 flex items-center justify-center text-2xl">
|
||||
<i class="uil uil-github-alt"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs font-bold text-slate-400 block uppercase">Git Tanımlayan</span>
|
||||
<span class="text-2xl font-extrabold text-slate-800">{{ $allInterns->whereNotNull('github_repo')->count() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
<!-- Top header / Welcome card -->
|
||||
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50 flex flex-col md:flex-row justify-between items-start md:items-center gap-6 mb-8">
|
||||
<div>
|
||||
<span class="text-xs uppercase font-extrabold tracking-widest text-[#e31e24] bg-red-50 px-3 py-1.5 rounded-full">Yönetici Paneli</span>
|
||||
<h1 class="text-3xl font-extrabold text-slate-900 tracking-tight mt-3">Hoş Geldiniz, Yöneticimiz</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Stajyer başvurularını, takvimlerini ve proje depolarını buradan takip edebilirsiniz.</p>
|
||||
</div>
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<button onclick="toggleGuideModal(true)" class="px-5 py-3 rounded-xl bg-blue-50 hover:bg-blue-100 text-blue-600 font-extrabold text-sm transition-all flex items-center gap-2 border border-blue-100">
|
||||
<i class="uil uil-book-open text-lg"></i>
|
||||
<span>Stajyer Rehberi</span>
|
||||
</button>
|
||||
<a href="{{ route('homepage') }}" class="px-5 py-3 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-600 font-bold text-sm transition-all flex items-center gap-2 border border-slate-200/50">
|
||||
<i class="uil uil-home text-lg"></i>
|
||||
<span>Ana Sayfa</span>
|
||||
</a>
|
||||
<form action="{{ route('intern.admin.logout') }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="px-5 py-3 rounded-xl bg-red-50 hover:bg-red-100 text-[#e31e24] font-bold text-sm transition-all flex items-center gap-2 border border-red-100">
|
||||
<i class="uil uil-sign-out-alt text-lg"></i>
|
||||
<span>Çıkış Yap</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,442 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Staj Defteri - {{ $intern->name }}</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: 'Outfit', 'Segoe UI', sans-serif;
|
||||
color: #1e293b;
|
||||
background-color: #f8fafc;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
/* Page setup depending on chosen size */
|
||||
@page {
|
||||
@if($size === 'a5')
|
||||
size: A5 portrait;
|
||||
margin: 10mm;
|
||||
@else
|
||||
size: A4 portrait;
|
||||
margin: 15mm;
|
||||
@endif
|
||||
}
|
||||
|
||||
.notebook-page {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
||||
page-break-after: always;
|
||||
break-after: page;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
||||
@if($size === 'a5')
|
||||
width: 148mm;
|
||||
height: 210mm;
|
||||
padding: 12mm;
|
||||
font-size: 11px;
|
||||
@else
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
padding: 20mm;
|
||||
font-size: 13px;
|
||||
@endif
|
||||
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
/* Adjustments for actual print mode */
|
||||
@media print {
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.notebook-page {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Top control bar */
|
||||
.control-bar {
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #2563eb;
|
||||
color: #ffffff;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
.btn-secondary {
|
||||
background-color: #f1f5f9;
|
||||
color: #475569;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
.control-title {
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
.control-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Page Layout Styles */
|
||||
.header-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.header-table td {
|
||||
border: 1px solid #cbd5e1;
|
||||
padding: 8px 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.logo-cell {
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
.logo-img {
|
||||
max-height: 40px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.company-title {
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
color: #0f172a;
|
||||
line-height: 1.3;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.doc-title {
|
||||
font-weight: 800;
|
||||
color: #2563eb;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.meta-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.meta-table td {
|
||||
border: 1px solid #cbd5e1;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.meta-label {
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
background-color: #f8fafc;
|
||||
width: 25%;
|
||||
}
|
||||
.meta-value {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.day-banner {
|
||||
background: linear-gradient(135deg, #eff6ff, #dbeafe);
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.day-badge {
|
||||
font-weight: 800;
|
||||
color: #1d4ed8;
|
||||
font-size: 14px;
|
||||
}
|
||||
.day-date {
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex-grow: 1;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #ffffff;
|
||||
margin-bottom: 20px;
|
||||
min-height: 250px;
|
||||
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;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-top: auto;
|
||||
border-t: 1px solid #e2e8f0;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.qr-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.qr-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.qr-text {
|
||||
font-size: 9px;
|
||||
color: #64748b;
|
||||
max-width: 120px;
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.signatures-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.signature-box {
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
background: #f8fafc;
|
||||
min-height: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.sig-title {
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.sig-status {
|
||||
font-weight: 800;
|
||||
color: #059669;
|
||||
margin: 6px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.sig-status.waiting {
|
||||
color: #d97706;
|
||||
}
|
||||
.sig-badge {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 7px;
|
||||
font-weight: 800;
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.sig-badge.waiting {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
.sig-name {
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Control Bar -->
|
||||
<div class="control-bar no-print">
|
||||
<div class="control-title">Staj Defteri Önizleme ({{ $size === 'a5' ? 'A5 Boyutu' : 'A4 Boyutu' }})</div>
|
||||
<div class="control-actions">
|
||||
<a href="{{ route('intern.dashboard') }}" class="btn btn-secondary">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||
<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') : '' }}{{ 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Render Each Day -->
|
||||
@foreach($days as $day)
|
||||
@php
|
||||
$entry = $savedEntries->get($day['day_number']);
|
||||
$dayNum = $day['day_number'];
|
||||
$dayDate = $day['date'];
|
||||
$dayDateFormatted = $day['formatted_date'];
|
||||
|
||||
// Build the verification QR code URL pointing to the verification page
|
||||
$verifyUrl = route('internship.verify', $intern->certificate_code) . '?day=' . $dayNum;
|
||||
$qrCodeUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=' . urlencode($verifyUrl);
|
||||
@endphp
|
||||
|
||||
<div class="notebook-page">
|
||||
<div>
|
||||
<!-- Header Card -->
|
||||
<table class="header-table">
|
||||
<tr>
|
||||
<td class="logo-cell">
|
||||
<img src="{{ asset('assets/img/truncgil-yatay.svg') }}" class="logo-img" alt="Logo">
|
||||
</td>
|
||||
<td>
|
||||
<div class="company-title">TRUNÇGİL TEKNOLOJİ</div>
|
||||
<div style="font-size: 8px; color: #64748b; margin-top: 2px;">Gaziantep Teknopark, Şahinbey / Gaziantep</div>
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<div class="doc-title">STAJ GÜNLÜK RAPORU</div>
|
||||
<div style="font-size: 8px; color: #64748b; margin-top: 2px;">Staj Defteri Sayfası</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Student & Metadata Grid -->
|
||||
<table class="meta-table">
|
||||
<tr>
|
||||
<td class="meta-label">Öğrenci Ad Soyad</td>
|
||||
<td class="meta-value">{{ $intern->name }}</td>
|
||||
<td class="meta-label">Staj Türü / Deposu</td>
|
||||
<td class="meta-value">{{ $intern->github_repo ? 'GitHub Projeli Staj' : 'Genel Staj' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="meta-label">Staj Başlangıç</td>
|
||||
<td class="meta-value">{{ \Carbon\Carbon::parse($intern->internship_start_date)->format('d.m.Y') }}</td>
|
||||
<td class="meta-label">Toplam İş Günü</td>
|
||||
<td class="meta-value">{{ $intern->internship_total_days }} Gün</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Day Header Banner -->
|
||||
<div class="day-banner">
|
||||
<span class="day-badge">{{ $dayNum }}. Gün Raporu</span>
|
||||
<span class="day-date"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block; vertical-align:middle; margin-right:4px;"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>{{ $dayDateFormatted }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="content-area">
|
||||
@if($entry && trim($entry->content))
|
||||
{!! $entry->content !!}
|
||||
@else
|
||||
<div class="empty-content">Bu gün için herhangi bir staj raporu girilmemiştir.</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer & Signatures Block -->
|
||||
<div class="footer-section">
|
||||
<!-- QR Code Block -->
|
||||
<div class="qr-block">
|
||||
<img src="{{ $qrCodeUrl }}" class="qr-img" alt="QR Code">
|
||||
<div class="qr-text">
|
||||
<strong>KOD DOĞRULAMA</strong><br>
|
||||
Bu staj sayfası webden doğrulanabilir. Doğrulamak için QR kodu taratın.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="signatures-container">
|
||||
<!-- Supervisor -->
|
||||
<div class="signature-box">
|
||||
<div class="sig-title">Staj Sorumlusu</div>
|
||||
@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">{{ $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>
|
||||
</div>
|
||||
<div class="sig-name"> </div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,679 @@
|
||||
@extends('layouts.site')
|
||||
|
||||
@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)
|
||||
<div class="bg-slate-100 py-12 no-print">
|
||||
<div class="container max-w-6xl mx-auto px-4">
|
||||
<!-- Verification Banner (e-Devlet Style) -->
|
||||
<div class="bg-emerald-50 border-l-4 border-emerald-500 p-4 rounded-xl shadow-sm mb-6 flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div class="w-10 h-10 rounded-full bg-emerald-500 text-white flex items-center justify-center flex-shrink-0 shadow-md shadow-emerald-500/10">
|
||||
<i class="uil uil-shield-check text-xl"></i>
|
||||
</div>
|
||||
<div class="text-sm leading-normal">
|
||||
<span class="font-bold text-slate-800">Güvenli Belge Doğrulama:</span>
|
||||
<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">
|
||||
<!-- 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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Printable Area -->
|
||||
<div class="print-container bg-slate-100 py-6 min-h-screen">
|
||||
<div class="container max-w-6xl mx-auto px-4 pb-20 space-y-12">
|
||||
|
||||
<!-- ================= PAGE 1: CERTIFICATE (LANDSCAPE A4) ================= -->
|
||||
<div class="certificate-container-wrapper overflow-hidden w-full flex justify-center items-start">
|
||||
<div id="certificate-node" class="certificate-page bg-white rounded-[2rem] shadow-2xl p-12 relative overflow-hidden page-break mx-auto">
|
||||
|
||||
<!-- Watermark Logo -->
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none z-0 opacity-[0.05]">
|
||||
<img src="{{ asset('logos/truncgil-dikey.svg') }}" class="w-[300px] h-[300px] object-contain">
|
||||
</div>
|
||||
|
||||
<!-- Thin Minimal Orange/Slate Inner Border -->
|
||||
|
||||
<div class="relative z-10 flex flex-col justify-between h-full min-h-[580px] md:min-h-[540px]">
|
||||
|
||||
<!-- Logo & Header -->
|
||||
<div class="relative flex justify-between items-center w-full min-h-[64px]">
|
||||
<div class="w-1/3"></div>
|
||||
<div class="w-1/3 flex justify-center">
|
||||
<img src="{{ asset('logos/truncgil-yatay.svg') }}" alt="Trunçgil Teknoloji" class="h-16 md:h-20 object-contain">
|
||||
</div>
|
||||
<div class="w-1/3 text-right">
|
||||
<span class="text-[10px] font-bold text-slate-400 tracking-widest block">VERİFİKASYON NO</span>
|
||||
<span class="text-xs font-mono font-bold text-slate-800">{{ $application->certificate_code }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="text-center my-auto px-4">
|
||||
<h1 class="font-cinzel text-3xl md:text-5xl text-slate-900 mt-8 mb-2" style="font-weight: 900; letter-spacing: 0.04em;">STAJ BİTİRME<br>SERTİFİKASI</h1>
|
||||
|
||||
<p class="text-slate-400 text-xs md:text-sm tracking-wider mb-0">SAYIN</p>
|
||||
<h2 class="font-cinzel text-2xl md:text-4xl font-extrabold text-slate-900 tracking-wide mb-6 mt-0">{{ $application->name }}</h2>
|
||||
|
||||
<div class="max-w-3xl mx-auto text-slate-700 leading-relaxed text-sm md:text-base space-y-4">
|
||||
<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 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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section (Signatures & Verification QR) -->
|
||||
<div class="grid grid-cols-3 items-end pt-6 border-t border-slate-100/50">
|
||||
<!-- Left: QR Code -->
|
||||
<div class="flex items-center gap-4">
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=150x150&data={{ urlencode(route('internship.verify', $application->certificate_code)) }}" alt="QR Code" class="w-28 h-28 border border-slate-200 p-1 rounded-xl bg-white shadow-sm">
|
||||
<div class="text-left hidden sm:block">
|
||||
<span class="text-[8px] font-bold text-slate-400 tracking-wider block">DİJİTAL DOĞRULAMA</span>
|
||||
<span class="text-[9px] text-slate-500 block">Karekod okutularak belgenin güncelliği doğrulanabilir.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle: Date -->
|
||||
<div class="text-center">
|
||||
<span class="text-[9px] text-slate-600 block tracking-wider text-glow-white font-semibold">DÜZENLENME TARİHİ</span>
|
||||
<span class="text-xs font-bold text-slate-800 text-glow-white">{{ \Carbon\Carbon::parse($application->internship_end_date)->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= PAGE 2: TRANSCRIPT (PORTRAIT A4) ================= -->
|
||||
<div class="transcript-container-wrapper overflow-hidden w-full flex justify-center items-start">
|
||||
<div id="transcript-node" class="transcript-page bg-white rounded-[2rem] shadow-2xl p-6 md:p-8 relative overflow-hidden mx-auto">
|
||||
|
||||
<!-- Thin border decor for transcript -->
|
||||
<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">STAJ AKADEMİK TRANSKRİPTİ</h2>
|
||||
<p class="text-slate-400 tracking-wider mt-0.5 transcript-header-subtitle">VE PERFORMANS DEĞERLENDİRME RAPORU</p>
|
||||
</div>
|
||||
<img src="{{ asset('logos/truncgil-yatay.svg') }}" alt="Trunçgil Logo" class="h-6 object-contain">
|
||||
</div>
|
||||
|
||||
<!-- Student Info block -->
|
||||
<div class="bg-slate-50 rounded-xl p-2.5 border border-slate-100 mb-3">
|
||||
<h3 class="font-bold text-slate-400 tracking-widest mb-1.5 student-info-title">ÖĞRENCİ / STAJYER BİLGİLERİ</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-1 gap-x-8 student-info-grid">
|
||||
<div class="flex justify-between border-b border-slate-200/50 pb-1">
|
||||
<span class="text-slate-500">Adı Soyadı:</span>
|
||||
<strong class="text-slate-800">{{ $application->name }}</strong>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-slate-200/50 pb-1">
|
||||
<span class="text-slate-500">E-Posta Adresi:</span>
|
||||
<strong class="text-slate-800">{{ $application->email }}</strong>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-slate-200/50 pb-1">
|
||||
<span class="text-slate-500">Staj Dönemi:</span>
|
||||
<strong class="text-slate-800">{{ \Carbon\Carbon::parse($application->internship_start_date)->format('d.m.Y') }} - {{ \Carbon\Carbon::parse($application->internship_end_date)->format('d.m.Y') }}</strong>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-slate-200/50 pb-1">
|
||||
<span class="text-slate-500">Toplam Staj Süresi:</span>
|
||||
<strong class="text-slate-800">{{ $application->internship_total_days }} İş Günü</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Markdown Content (Transcript and marks) -->
|
||||
<div class="prose max-w-none text-slate-700 leading-relaxed mb-3">
|
||||
{!! new \Illuminate\Support\HtmlString(\Illuminate\Support\Str::markdown($application->transcript_markdown ?? '')) !!}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer with verification details -->
|
||||
<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>
|
||||
|
||||
<!-- ================= 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>
|
||||
: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;
|
||||
}
|
||||
.font-serif {
|
||||
font-family: 'Cinzel', 'Playfair Display', Georgia, serif;
|
||||
}
|
||||
.font-cinzel {
|
||||
font-family: 'Cinzel', serif !important;
|
||||
}
|
||||
|
||||
.text-glow-white {
|
||||
text-shadow: 0 0 10px #ffffff, 0 0 6px #ffffff, 0 0 3px #ffffff;
|
||||
}
|
||||
|
||||
/* Transcript typography helpers scaled globally */
|
||||
.transcript-header-title {
|
||||
font-size: calc(0.875rem * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
.transcript-header-subtitle {
|
||||
font-size: calc(7px * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
.student-info-title {
|
||||
font-size: calc(7.5px * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
.student-info-grid {
|
||||
font-size: calc(8.5px * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
.verify-code-label {
|
||||
font-size: calc(6.5px * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
.verify-code {
|
||||
font-size: calc(9px * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
.footer-address {
|
||||
font-size: calc(7.5px * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
|
||||
/* Screen display aspect styling */
|
||||
.certificate-page {
|
||||
width: 1122px;
|
||||
min-height: 794px;
|
||||
box-sizing: border-box;
|
||||
background-image: url('{{ asset("assets/img/cert_bg_futuristic.png") }}');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.transcript-page {
|
||||
width: 794px;
|
||||
height: 1122px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Elegant Markdown Table Styling inside prose (Reduced font sizes & paddings) */
|
||||
.prose table {
|
||||
width: 100%;
|
||||
margin-top: 0.35rem !important;
|
||||
margin-bottom: 0.35rem !important;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
.prose th {
|
||||
font-weight: 700;
|
||||
padding: 0.2rem 0.35rem !important;
|
||||
background-color: #f8fafc;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
color: #1e293b;
|
||||
font-size: calc(0.45rem * var(--transcript-font-scale)) !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.prose td {
|
||||
padding: 0.2rem 0.35rem !important;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
color: #334155;
|
||||
font-size: calc(0.45rem * var(--transcript-font-scale)) !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
.prose tr:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
.prose h3 {
|
||||
font-family: 'Cinzel', serif;
|
||||
color: #0f172a;
|
||||
margin-top: 0.5rem !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding-bottom: 0.15rem;
|
||||
font-weight: 700;
|
||||
font-size: calc(0.6rem * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
.prose h4 {
|
||||
color: #1e293b;
|
||||
margin-top: 0.4rem !important;
|
||||
margin-bottom: 0.2rem !important;
|
||||
font-weight: 600;
|
||||
font-size: calc(0.52rem * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
.prose p {
|
||||
font-size: calc(0.45rem * var(--transcript-font-scale)) !important;
|
||||
line-height: 1.3 !important;
|
||||
margin-top: 0.15rem !important;
|
||||
margin-bottom: 0.2rem !important;
|
||||
}
|
||||
.prose ul {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.1rem 0.75rem;
|
||||
padding-left: 0 !important;
|
||||
margin-top: 0.25rem !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
list-style-type: none !important;
|
||||
}
|
||||
.prose ul li {
|
||||
font-size: calc(0.45rem * var(--transcript-font-scale)) !important;
|
||||
line-height: 1.3 !important;
|
||||
margin: 0 !important;
|
||||
position: relative;
|
||||
padding-left: 0.5rem !important;
|
||||
}
|
||||
.prose ul li::before {
|
||||
content: "•";
|
||||
color: #f97316;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.prose blockquote {
|
||||
border-left: 2px solid #f97316;
|
||||
padding-left: 0.4rem !important;
|
||||
font-style: italic;
|
||||
color: #475569;
|
||||
margin: 0.4rem 0 !important;
|
||||
font-size: calc(0.45rem * var(--transcript-font-scale)) !important;
|
||||
}
|
||||
|
||||
/* Print media config for mixed Landscape & Portrait */
|
||||
@media print {
|
||||
@page {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@page landscape-page {
|
||||
size: A4 landscape;
|
||||
}
|
||||
|
||||
@page portrait-page {
|
||||
size: A4 portrait;
|
||||
}
|
||||
|
||||
body, html {
|
||||
background-color: #ffffff !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.no-print, header, footer, nav, .site-header, .site-footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.print-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100% !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.certificate-container-wrapper, .transcript-container-wrapper {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.certificate-page {
|
||||
page: landscape-page;
|
||||
transform: none !important;
|
||||
width: 297mm !important;
|
||||
height: 210mm !important;
|
||||
min-height: 210mm !important;
|
||||
margin: 0 !important;
|
||||
padding: 15mm !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
page-break-after: always !important;
|
||||
break-after: page !important;
|
||||
background-image: url('{{ asset("assets/img/cert_bg_futuristic.png") }}') !important;
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-color: #ffffff !important;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
.transcript-page {
|
||||
page: portrait-page;
|
||||
transform: none !important;
|
||||
width: 210mm !important;
|
||||
height: 297mm !important;
|
||||
min-height: 297mm !important;
|
||||
margin: 0 !important;
|
||||
padding: 10mm !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function scaleDocument(wrapperSelector, nodeSelector, baseWidth, baseHeight) {
|
||||
const wrapper = document.querySelector(wrapperSelector);
|
||||
const node = document.querySelector(nodeSelector);
|
||||
if (!wrapper || !node) return;
|
||||
|
||||
const containerWidth = wrapper.offsetWidth;
|
||||
if (containerWidth < baseWidth) {
|
||||
const scale = containerWidth / baseWidth;
|
||||
node.style.transform = `scale(${scale})`;
|
||||
node.style.transformOrigin = 'top center';
|
||||
node.style.width = `${baseWidth}px`;
|
||||
node.style.minWidth = `${baseWidth}px`;
|
||||
node.style.height = `${baseHeight}px`;
|
||||
node.style.minHeight = `${baseHeight}px`;
|
||||
wrapper.style.height = `${baseHeight * scale}px`;
|
||||
} else {
|
||||
node.style.transform = '';
|
||||
node.style.transformOrigin = '';
|
||||
node.style.width = '';
|
||||
node.style.minWidth = '';
|
||||
node.style.height = '';
|
||||
node.style.minHeight = '';
|
||||
wrapper.style.height = '';
|
||||
}
|
||||
}
|
||||
|
||||
function scaleAllDocuments() {
|
||||
scaleDocument('.certificate-container-wrapper', '#certificate-node', 1122, 794);
|
||||
scaleDocument('.transcript-container-wrapper', '#transcript-node', 794, 1122);
|
||||
scaleDocument('.notebook-container-wrapper', '#notebook-node', 794, 1122);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', scaleAllDocuments);
|
||||
window.addEventListener('DOMContentLoaded', scaleAllDocuments);
|
||||
window.addEventListener('load', scaleAllDocuments);
|
||||
|
||||
// Immediate and delayed triggers to ensure correct measurement after image & stylesheet loading
|
||||
scaleAllDocuments();
|
||||
setTimeout(scaleAllDocuments, 100);
|
||||
setTimeout(scaleAllDocuments, 500);
|
||||
setTimeout(scaleAllDocuments, 1500);
|
||||
|
||||
async function downloadPDF() {
|
||||
const btn = document.getElementById('pdf-btn');
|
||||
const icon = document.getElementById('pdf-icon');
|
||||
const spinner = document.getElementById('pdf-spinner');
|
||||
const btnText = document.getElementById('pdf-btn-text');
|
||||
|
||||
// Show spinner
|
||||
btn.disabled = true;
|
||||
icon.classList.add('hidden');
|
||||
spinner.classList.remove('hidden');
|
||||
btnText.textContent = "PDF Hazırlanıyor...";
|
||||
|
||||
try {
|
||||
const { jsPDF } = window.jspdf;
|
||||
|
||||
const certEl = document.getElementById('certificate-node');
|
||||
const transEl = document.getElementById('transcript-node');
|
||||
const notebookEl = document.getElementById('notebook-node');
|
||||
|
||||
// Force elements to normal scale prior to capturing for PDF
|
||||
const origCertTransform = certEl.style.transform;
|
||||
const origCertWidth = certEl.style.width;
|
||||
const origCertHeight = certEl.style.height;
|
||||
const origCertRadius = certEl.style.borderRadius;
|
||||
const origTransTransform = transEl.style.transform;
|
||||
const origTransWidth = transEl.style.width;
|
||||
const origTransHeight = transEl.style.height;
|
||||
const origTransRadius = transEl.style.borderRadius;
|
||||
|
||||
let origNotebookTransform, origNotebookWidth, origNotebookHeight, origNotebookRadius;
|
||||
if (notebookEl) {
|
||||
origNotebookTransform = notebookEl.style.transform;
|
||||
origNotebookWidth = notebookEl.style.width;
|
||||
origNotebookHeight = notebookEl.style.height;
|
||||
origNotebookRadius = notebookEl.style.borderRadius;
|
||||
|
||||
notebookEl.style.transform = 'none';
|
||||
notebookEl.style.width = '794px';
|
||||
notebookEl.style.height = '1122px';
|
||||
notebookEl.style.borderRadius = '0';
|
||||
}
|
||||
|
||||
certEl.style.transform = 'none';
|
||||
certEl.style.width = '1122px';
|
||||
certEl.style.height = '794px';
|
||||
certEl.style.borderRadius = '0';
|
||||
transEl.style.transform = 'none';
|
||||
transEl.style.width = '794px';
|
||||
transEl.style.height = '1122px';
|
||||
transEl.style.borderRadius = '0';
|
||||
|
||||
// Render Page 1 (Landscape)
|
||||
const certCanvas = await html2canvas(certEl, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
backgroundColor: '#ffffff',
|
||||
logging: false
|
||||
});
|
||||
const certImgData = certCanvas.toDataURL('image/jpeg', 1.0);
|
||||
|
||||
// Initialize landscape A4 document
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'landscape',
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
pdf.addImage(certImgData, 'JPEG', 0, 0, 297, 210);
|
||||
|
||||
// Render Page 2 (Portrait)
|
||||
const transCanvas = await html2canvas(transEl, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
backgroundColor: '#ffffff',
|
||||
logging: false
|
||||
});
|
||||
const transImgData = transCanvas.toDataURL('image/jpeg', 1.0);
|
||||
|
||||
// Calculate exact height using aspect ratio to prevent vertical squishing
|
||||
const transWidth = 210;
|
||||
const transHeight = (transCanvas.height * transWidth) / transCanvas.width;
|
||||
|
||||
// Add portrait page to PDF
|
||||
pdf.addPage('a4', 'portrait');
|
||||
pdf.addImage(transImgData, 'JPEG', 0, 0, transWidth, transHeight);
|
||||
|
||||
// Render Page 3 (Notebook) if it exists
|
||||
if (notebookEl) {
|
||||
const notebookCanvas = await html2canvas(notebookEl, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
backgroundColor: '#ffffff',
|
||||
logging: false
|
||||
});
|
||||
const notebookImgData = notebookCanvas.toDataURL('image/jpeg', 1.0);
|
||||
const notebookHeight = (notebookCanvas.height * transWidth) / notebookCanvas.width;
|
||||
|
||||
pdf.addPage('a4', 'portrait');
|
||||
pdf.addImage(notebookImgData, 'JPEG', 0, 0, transWidth, notebookHeight);
|
||||
}
|
||||
|
||||
// Restore styles
|
||||
certEl.style.transform = origCertTransform;
|
||||
certEl.style.width = origCertWidth;
|
||||
certEl.style.height = origCertHeight;
|
||||
certEl.style.borderRadius = origCertRadius;
|
||||
transEl.style.transform = origTransTransform;
|
||||
transEl.style.width = origTransWidth;
|
||||
transEl.style.height = origTransHeight;
|
||||
transEl.style.borderRadius = origTransRadius;
|
||||
|
||||
if (notebookEl) {
|
||||
notebookEl.style.transform = origNotebookTransform;
|
||||
notebookEl.style.width = origNotebookWidth;
|
||||
notebookEl.style.height = origNotebookHeight;
|
||||
notebookEl.style.borderRadius = origNotebookRadius;
|
||||
}
|
||||
|
||||
// Trigger Save File
|
||||
pdf.save('{{ str()->slug($application->name) }}-staj-belgesi.pdf');
|
||||
|
||||
} catch (error) {
|
||||
console.error("PDF generation failed", error);
|
||||
alert("PDF oluşturulurken bir hata meydana geldi.");
|
||||
} finally {
|
||||
// Restore button
|
||||
btn.disabled = false;
|
||||
icon.classList.remove('hidden');
|
||||
spinner.classList.add('hidden');
|
||||
btnText.textContent = "PDF Olarak İndir";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@@ -14,7 +14,8 @@
|
||||
<link rel="preload" href="{{ asset('html/assets/css/fonts/urbanist.css') }}" as="style" onload="this.rel='stylesheet'">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=translate">
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=translate&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=translate&display=swap" rel="stylesheet"></noscript>
|
||||
@stack('styles')
|
||||
<style>
|
||||
.navbar.navbar-light.fixed .btn:not(.btn-expand):not(.btn-gradient) {
|
||||
|
||||
@@ -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 }}">
|
||||
@@ -58,6 +77,15 @@ $favicon_path = setting('site_favicon');
|
||||
|
||||
<link rel="icon" href="{{ $favicon_path ? (str_starts_with($favicon_path, 'http') ? $favicon_path : asset($favicon_path)) : asset('assets/img/favicon.png') }}">
|
||||
@stack('head')
|
||||
@if(request()->routeIs('homepage'))
|
||||
@php
|
||||
$hero_bg_preload = setting('hero_bg') ?: './assets/img/photos/bg16.webp';
|
||||
if ($hero_bg_preload && !str_starts_with($hero_bg_preload, 'http')) {
|
||||
$hero_bg_preload = asset($hero_bg_preload);
|
||||
}
|
||||
@endphp
|
||||
<link rel="preload" href="{{ $hero_bg_preload }}" as="image" fetchpriority="high">
|
||||
@endif
|
||||
<link rel="stylesheet" href="{{ asset('html/style.css') }}">
|
||||
<link rel="preload" href="{{ asset('assets/css/icon.css') }}" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="{{ asset('assets/css/icon.css') }}"></noscript>
|
||||
@@ -70,8 +98,10 @@ $favicon_path = setting('site_favicon');
|
||||
<link rel="preload" href="{{ asset('assets/css/fonts/space.css') }}" as="style" onload="this.rel='stylesheet'">
|
||||
<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=Caveat:wght@600&family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=translate">
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Caveat:wght@600&family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=Caveat:wght@600&family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet"></noscript>
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=translate&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=translate&display=swap" rel="stylesheet"></noscript>
|
||||
|
||||
<!-- Site Verification -->
|
||||
@if(setting('seo_google_search_console'))
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
<div
|
||||
x-data="{
|
||||
githubRepoUrl: @js($this->githubRepo),
|
||||
commitDays: [],
|
||||
activeDayIndex: 0,
|
||||
loading: false,
|
||||
errorMsg: '',
|
||||
isEmpty: false,
|
||||
|
||||
init() {
|
||||
this.loadCommits();
|
||||
},
|
||||
|
||||
loadCommits() {
|
||||
if (!this.githubRepoUrl) {
|
||||
this.isEmpty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.errorMsg = '';
|
||||
this.isEmpty = false;
|
||||
this.commitDays = [];
|
||||
|
||||
let repoClean = this.githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
|
||||
repoClean = repoClean.replace(/\/$/, '');
|
||||
const parts = repoClean.split('/');
|
||||
if (parts.length < 2) {
|
||||
this.loading = false;
|
||||
this.errorMsg = 'Github depo URL\'si çözümlenemedi. Geçerli format: https://github.com/kullanici/depo';
|
||||
return;
|
||||
}
|
||||
const owner = parts[0];
|
||||
const repo = parts[1].replace(/\.git$/i, '');
|
||||
|
||||
fetch('https://api.github.com/repos/' + owner + '/' + repo + '/commits?per_page=100')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) throw new Error('Depo bulunamadı veya private (gizli). Deponuzun public (herkese açık) olduğundan emin olun.');
|
||||
else if (response.status === 403) throw new Error('GitHub API istek limiti aşıldı. Lütfen daha sonra tekrar deneyin.');
|
||||
else throw new Error('GitHub API hatası: ' + response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(commits => {
|
||||
this.loading = false;
|
||||
if (!Array.isArray(commits) || commits.length === 0) {
|
||||
this.isEmpty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
commits.reverse();
|
||||
|
||||
const grouped = {};
|
||||
commits.forEach(item => {
|
||||
const dateStr = item.commit.author.date;
|
||||
if (dateStr) {
|
||||
const dateOnly = dateStr.substring(0, 10);
|
||||
if (!grouped[dateOnly]) grouped[dateOnly] = [];
|
||||
grouped[dateOnly].push(item);
|
||||
}
|
||||
});
|
||||
|
||||
const sortedDates = Object.keys(grouped).sort();
|
||||
|
||||
this.commitDays = sortedDates.map((date, index) => {
|
||||
const dp = date.split('-');
|
||||
return {
|
||||
date: date,
|
||||
dayNum: index + 1,
|
||||
formattedDate: dp[2] + '.' + dp[1] + '.' + dp[0],
|
||||
commits: grouped[date].map(c => ({
|
||||
message: c.commit.message,
|
||||
time: new Date(c.commit.author.date).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' }),
|
||||
sha: c.sha.substring(0, 7),
|
||||
url: c.html_url
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
if (this.commitDays.length === 0) {
|
||||
this.isEmpty = true;
|
||||
return;
|
||||
}
|
||||
this.activeDayIndex = 0;
|
||||
})
|
||||
.catch(err => {
|
||||
this.loading = false;
|
||||
this.errorMsg = err.message || 'Bir hata oluştu.';
|
||||
});
|
||||
},
|
||||
|
||||
get activeDay() {
|
||||
return this.commitDays[this.activeDayIndex] || null;
|
||||
},
|
||||
|
||||
navigateDay(dir) {
|
||||
const n = this.activeDayIndex + dir;
|
||||
if (n >= 0 && n < this.commitDays.length) this.activeDayIndex = n;
|
||||
},
|
||||
|
||||
selectDay(i) {
|
||||
this.activeDayIndex = i;
|
||||
}
|
||||
}"
|
||||
style="margin-top: 1rem; font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;"
|
||||
>
|
||||
<style>
|
||||
.jt-day-btn {
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
.jt-day-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
.jt-arrow-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.jt-arrow-btn:hover:not(:disabled) {
|
||||
transform: scale(1.08);
|
||||
background: #eff6ff;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
.jt-arrow-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.jt-event-card {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.jt-event-card:hover {
|
||||
transform: translateY(-2px) translateX(3px);
|
||||
box-shadow: 0 12px 24px -8px rgba(59, 130, 246, 0.12);
|
||||
border-color: #bfdbfe !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
.jt-timeline-dot {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.jt-event-row:hover .jt-timeline-dot {
|
||||
transform: scale(1.2);
|
||||
background-color: #3b82f6 !important;
|
||||
box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.18);
|
||||
}
|
||||
.jt-event-row:hover .jt-timeline-dot-inner {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
.jt-sha-link {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
.jt-sha-link:hover {
|
||||
background: #3b82f6 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
.jt-paginator-scroll::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
.jt-paginator-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.jt-paginator-scroll::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.jt-paginator-scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 transparent;
|
||||
}
|
||||
@keyframes jt-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes jt-fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.jt-fade-in {
|
||||
animation: jt-fadeIn 0.4s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{-- Loader --}}
|
||||
<div x-show="loading" x-cloak style="padding: 3rem 0; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div style="width: 2.5rem; height: 2.5rem; border: 2px solid #e2e8f0; border-top-color: #3b82f6; border-radius: 50%; animation: jt-spin 0.8s linear infinite; margin-bottom: 0.75rem;"></div>
|
||||
<span style="font-size: 0.75rem; color: #94a3b8; font-weight: 600; letter-spacing: 0.025em;">Commit geçmişi yükleniyor...</span>
|
||||
</div>
|
||||
|
||||
{{-- Error --}}
|
||||
<div x-show="errorMsg" x-cloak x-text="errorMsg" style="padding: 1rem 1.25rem; border-radius: 1rem; background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; font-size: 0.75rem; font-weight: 700;"></div>
|
||||
|
||||
{{-- Empty --}}
|
||||
<div x-show="isEmpty && !loading" x-cloak style="padding: 2rem; border-radius: 1.5rem; border: 1px solid #f1f5f9; background: #ffffff; text-align: center; color: #94a3b8; font-style: italic; font-size: 0.875rem;">
|
||||
Henüz hiçbir commit verisi bulunamadı veya depo adresi yapılandırılmadı.
|
||||
</div>
|
||||
|
||||
{{-- Day Paginator --}}
|
||||
<div x-show="commitDays.length > 0" x-cloak style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.75rem; background: rgba(255,255,255,0.8); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid #e2e8f0; border-radius: 1.5rem; margin-bottom: 1.75rem; user-select: none; box-shadow: 0 1px 3px rgba(0,0,0,0.04);">
|
||||
<button type="button" @click="navigateDay(-1)" :disabled="activeDayIndex === 0" class="jt-arrow-btn" style="width: 2.75rem; height: 2.75rem; border-radius: 0.875rem; background: #ffffff; color: #64748b; border: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; box-shadow: 0 1px 2px rgba(0,0,0,0.04);">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="jt-paginator-scroll" style="flex: 1; overflow-x: auto; padding: 0.375rem 0;">
|
||||
<div style="display: flex; gap: 0.625rem; justify-content: center; min-width: max-content; padding: 0 0.5rem;">
|
||||
<template x-for="(day, index) in commitDays" :key="day.date">
|
||||
<button type="button" @click="selectDay(index)" class="jt-day-btn" :style="index === activeDayIndex
|
||||
? 'padding: 0.5rem 1.25rem; border-radius: 0.875rem; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 1.5px solid #93c5fd; background: linear-gradient(135deg, #eff6ff, #dbeafe); color: #1d4ed8; font-weight: 800; box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);'
|
||||
: 'padding: 0.5rem 1.25rem; border-radius: 0.875rem; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 1px solid #e2e8f0; background: #ffffff; color: #64748b; font-weight: 600;'">
|
||||
<span style="font-size: 0.75rem; line-height: 1.2;" x-text="day.dayNum + '. Gün'"></span>
|
||||
<span style="font-size: 0.6rem; margin-top: 0.125rem; opacity: 0.7; font-weight: 500;" x-text="day.formattedDate"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" @click="navigateDay(1)" :disabled="activeDayIndex === commitDays.length - 1" class="jt-arrow-btn" style="width: 2.75rem; height: 2.75rem; border-radius: 0.875rem; background: #ffffff; color: #64748b; border: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; box-shadow: 0 1px 2px rgba(0,0,0,0.04);">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Active Day Card --}}
|
||||
<div x-show="activeDay" x-cloak class="jt-fade-in" :key="activeDayIndex" style="background: #ffffff; border: 1px solid #f1f5f9; border-radius: 1.5rem; padding: 1.75rem 2rem; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.06);">
|
||||
{{-- Day Header --}}
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.75rem; padding-bottom: 1.25rem; border-bottom: 1px solid #f1f5f9;">
|
||||
<div style="display: flex; align-items: center; gap: 0.875rem;">
|
||||
<span style="font-size: 0.875rem; font-weight: 800; color: #2563eb; background: linear-gradient(135deg, #eff6ff, #dbeafe); padding: 0.5rem 1.125rem; border-radius: 1rem; box-shadow: 0 1px 3px rgba(37, 99, 235, 0.1);" x-text="activeDay ? activeDay.dayNum + '. Gün' : ''"></span>
|
||||
<span style="font-size: 0.8125rem; font-weight: 600; color: #94a3b8; display: flex; align-items: center; gap: 0.375rem;">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: #60a5fa;"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||
<span x-text="activeDay ? activeDay.formattedDate : ''"></span>
|
||||
</span>
|
||||
</div>
|
||||
<span style="font-size: 0.6875rem; font-weight: 700; color: #94a3b8; background: #f8fafc; border: 1px solid #f1f5f9; padding: 0.375rem 0.875rem; border-radius: 0.625rem;" x-text="activeDay ? activeDay.commits.length + ' Commit' : ''"></span>
|
||||
</div>
|
||||
|
||||
{{-- Timeline --}}
|
||||
<div style="position: relative; padding-left: 2.25rem;">
|
||||
{{-- Gradient Timeline Line --}}
|
||||
<div style="position: absolute; left: 11px; top: 8px; bottom: 8px; width: 2px; background: linear-gradient(180deg, #3b82f6 0%, #818cf8 40%, #c7d2fe 100%); border-radius: 2px;"></div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 1.25rem;">
|
||||
<template x-for="(commit, ci) in (activeDay ? activeDay.commits : [])" :key="ci">
|
||||
<div class="jt-event-row" style="position: relative;">
|
||||
{{-- Timeline Dot --}}
|
||||
<div class="jt-timeline-dot" style="position: absolute; left: -32px; top: 6px; width: 24px; height: 24px; border-radius: 50%; background: #ffffff; border: 2.5px solid #3b82f6; display: flex; align-items: center; justify-content: center; z-index: 1;">
|
||||
<div class="jt-timeline-dot-inner" style="width: 8px; height: 8px; border-radius: 50%; background: #3b82f6;"></div>
|
||||
</div>
|
||||
{{-- Event Card --}}
|
||||
<div class="jt-event-card" style="padding: 1.125rem 1.25rem; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.03);">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 0.625rem;">
|
||||
<span style="font-size: 0.75rem; font-weight: 700; color: #94a3b8; display: flex; align-items: center; gap: 0.375rem;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#60a5fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span x-text="commit.time"></span>
|
||||
</span>
|
||||
<a :href="commit.url" target="_blank" class="jt-sha-link" x-text="commit.sha" style="font-size: 0.6875rem; font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 700; color: #2563eb; background: #eff6ff; padding: 0.25rem 0.75rem; border-radius: 0.5rem; border: 1px solid #bfdbfe; text-decoration: none;"></a>
|
||||
</div>
|
||||
<p style="font-size: 0.8125rem; font-weight: 600; color: #334155; line-height: 1.6; white-space: pre-line; margin: 0;" x-text="commit.message"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
@if(!empty($social_media['Twitter']))
|
||||
<a class="!text-white hover:!text-[#5daed5] transition-all duration-[0.2s] ease-in-out translate-y-0 hover:translate-y-[-0.15rem] mx-3" style="font-size: 2.25rem !important;" href="{{ $social_media['Twitter'] }}" target="_blank" rel="noopener noreferrer" aria-label="{{ t('Twitter') }}"><i class="uil uil-twitter before:content-['\ed59']" style="font-size: 2.25rem !important;" aria-hidden="true"></i></a>
|
||||
@endif
|
||||
@if(!empty($social_media['Facebook']))
|
||||
<a class="!text-white hover:!text-[#4267B2] transition-all duration-[0.2s] ease-in-out translate-y-0 hover:translate-y-[-0.15rem] mx-3" style="font-size: 2.25rem !important;" href="{{ $social_media['Facebook'] }}" target="_blank" rel="noopener noreferrer" aria-label="{{ t('Facebook') }}"><i class="uil uil-facebook-f before:content-['\eae2']" style="font-size: 2.25rem !important;" aria-hidden="true"></i></a>
|
||||
@endif
|
||||
|
||||
@if(!empty($social_media['Instagram']))
|
||||
<a class="!text-white hover:!text-[#c13584] transition-all duration-[0.2s] ease-in-out translate-y-0 hover:translate-y-[-0.15rem] mx-3" style="font-size: 2.25rem !important;" href="{{ $social_media['Instagram'] }}" target="_blank" rel="noopener noreferrer" aria-label="{{ t('Instagram') }}"><i class="uil uil-instagram before:content-['\eb9c']" style="font-size: 2.25rem !important;" aria-hidden="true"></i></a>
|
||||
@endif
|
||||
@@ -33,6 +31,7 @@
|
||||
@php
|
||||
$google_play = $social_media['googleplay'] ?? $social_media['google_play'] ?? $social_media['GooglePlay'] ?? $social_media['Googleplay'] ?? '';
|
||||
$apple_store = $social_media['applestore'] ?? $social_media['apple_store'] ?? $social_media['AppleStore'] ?? $social_media['Applestore'] ?? $social_media['apple'] ?? $social_media['Apple'] ?? '';
|
||||
$microsoft_store = $social_media['microsoftstore'] ?? $social_media['microsoft_store'] ?? $social_media['MicrosoftStore'] ?? $social_media['Microsoftstore'] ?? 'https://apps.microsoft.com/search/publisher?name=Truncgil+Teknoloji&hl=tr-TR&gl=TR';
|
||||
@endphp
|
||||
@if(!empty($google_play))
|
||||
<a class="!text-white hover:!text-[#34A853] transition-all duration-[0.2s] ease-in-out translate-y-0 hover:translate-y-[-0.15rem] mx-3" style="font-size: 2.25rem !important;" href="{{ $google_play }}" target="_blank" rel="noopener noreferrer" aria-label="{{ t('Google Play') }}"><i class="uil uil-google-play before:content-['\eb4f']" style="font-size: 2.25rem !important;" aria-hidden="true"></i></a>
|
||||
@@ -40,6 +39,9 @@
|
||||
@if(!empty($apple_store))
|
||||
<a class="!text-white hover:!text-[#A2AAAD] transition-all duration-[0.2s] ease-in-out translate-y-0 hover:translate-y-[-0.15rem] mx-3" style="font-size: 2.25rem !important;" href="{{ $apple_store }}" target="_blank" rel="noopener noreferrer" aria-label="{{ t('App Store') }}"><i class="uil uil-apple before:content-['\e938']" style="font-size: 2.25rem !important;" aria-hidden="true"></i></a>
|
||||
@endif
|
||||
@if(!empty($microsoft_store))
|
||||
<a class="!text-white hover:!text-[#00A4EF] transition-all duration-[0.2s] ease-in-out translate-y-0 hover:translate-y-[-0.15rem] mx-3" style="font-size: 2.25rem !important;" href="{{ $microsoft_store }}" target="_blank" rel="noopener noreferrer" aria-label="{{ t('Microsoft Store') }}"><i class="uil uil-microsoft before:content-['\ec03']" style="font-size: 2.25rem !important;" aria-hidden="true"></i></a>
|
||||
@endif
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,4 +78,27 @@
|
||||
left: 50%;
|
||||
transform: translateX(-50%) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.image-wrapper {
|
||||
background-image: none !important;
|
||||
position: relative !important;
|
||||
}
|
||||
.image-wrapper::before {
|
||||
content: "" !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100vh !important;
|
||||
background-image: linear-gradient(rgba(30, 34, 40, 0.45), rgba(30, 34, 40, 0.45)), url('{{ $hero_bg }}') !important;
|
||||
background-size: cover !important;
|
||||
background-position: center center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
z-index: -1 !important;
|
||||
}
|
||||
.image-wrapper .divider {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,55 +1,51 @@
|
||||
<section class="wrapper !bg-[#ffffff]">
|
||||
<div class="container">
|
||||
<div class="flex flex-wrap mx-[-15px] xl:mx-[-12.5px] lg:mx-[-12.5px] md:mx-[-12.5px] !mb-[4.5rem] !mt-[-9rem] xl:!mb-[7rem] lg:!mb-[7rem] md:!mb-[7rem]">
|
||||
<div class="md:w-6/12 lg:w-6/12 xl:w-3/12 w-full flex-[0_0_auto] max-w-full !px-[15px] xl:!px-[12.5px] lg:!px-[12.5px] md:!px-[12.5px] !mt-[25px]">
|
||||
<div class="card glass-card !shadow-[0_0.25rem_1.75rem_rgba(30,34,40,0.07)] h-full flex flex-col">
|
||||
<div class="card-body flex-[1_1_auto] p-[40px] flex flex-col">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="svg-inject icon-svg !w-[2.2rem] !h-[2.2rem] solid-mono text-[#e668b3] text-fuchsia !mb-3"><path class="fill-primary" d="M152 0H29.33A29.32 29.32 0 000 29.33v165.33A29.32 29.32 0 0029.33 224h78.72l2.35-13.12a29.71 29.71 0 018.11-15.68l62.83-62.72V29.33A29.33 29.33 0 00152 0zM42.67 42.67h42.67a10.67 10.67 0 110 21.33H42.66a10.67 10.67 0 010-21.33zM96 149.33H42.67a10.67 10.67 0 110-21.33H96a10.67 10.67 0 110 21.33zm42.67-42.66h-96a10.67 10.67 0 010-21.34h96a10.67 10.67 0 110 21.34z"></path><path class="fill-secondary" d="M133.63 256a8 8 0 01-7.89-9.38l5.67-32.06a8 8 0 012.22-4.27l79.2-79.2c9.73-9.75 19.28-7.12 24.51-1.89l13.2 13.2a18.69 18.69 0 010 26.4l-79.2 79.2a7.83 7.83 0 01-4.27 2.22l-32 5.67a10.71 10.71 0 01-1.44.11zm32.05-13.65z"></path></svg>
|
||||
<h4>{!! t('Yapay Zeka') !!}</h4>
|
||||
<p class="!mb-2 flex-1 min-h-[4.5rem]">{!! t('Yapay zeka teknolojilerini kullanarak işlerinizi otomatikleştirebiliriz.') !!}</p>
|
||||
<a href="{{ route('page.show', ['slug' => 'yapay-zeka']) }}" class="more hover !text-[#e668b3] focus:!text-[#e668b3] hover:!text-[#e668b3] !mt-auto">{!! t('Daha fazla bilgi edin') !!}</a>
|
||||
<div class="flex flex-wrap glass-card-row !mb-[4.5rem] !mt-[-9rem] xl:!mb-[7rem] lg:!mb-[7rem] md:!mb-[7rem]">
|
||||
<div class="w-full md:w-6/12 lg:w-6/12 xl:w-3/12 flex-[0_0_auto] max-w-full glass-card-col">
|
||||
<a href="{{ route('page.show', ['slug' => 'yapay-zeka']) }}" class="card glass-card !shadow-[0_0.25rem_1.75rem_rgba(30,34,40,0.07)] h-full flex flex-col">
|
||||
<div class="card-body glass-card-body flex-[1_1_auto] flex flex-col">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="svg-inject icon-svg !w-[1.8rem] !h-[1.8rem] md:!w-[2.2rem] md:!h-[2.2rem] solid-mono text-[#e668b3] text-fuchsia !mb-2 md:!mb-3"><path class="fill-primary" d="M152 0H29.33A29.32 29.32 0 000 29.33v165.33A29.32 29.32 0 0029.33 224h78.72l2.35-13.12a29.71 29.71 0 018.11-15.68l62.83-62.72V29.33A29.33 29.33 0 00152 0zM42.67 42.67h42.67a10.67 10.67 0 110 21.33H42.66a10.67 10.67 0 010-21.33zM96 149.33H42.67a10.67 10.67 0 110-21.33H96a10.67 10.67 0 110 21.33zm42.67-42.66h-96a10.67 10.67 0 010-21.34h96a10.67 10.67 0 110 21.34z"></path><path class="fill-secondary" d="M133.63 256a8 8 0 01-7.89-9.38l5.67-32.06a8 8 0 012.22-4.27l79.2-79.2c9.73-9.75 19.28-7.12 24.51-1.89l13.2 13.2a18.69 18.69 0 010 26.4l-79.2 79.2a7.83 7.83 0 01-4.27 2.22l-32 5.67a10.71 10.71 0 01-1.44.11zm32.05-13.65z"></path></svg>
|
||||
<h4 class="!text-[14px] md:!text-[1.1rem] !mb-1 md:!mb-2">{!! t('Yapay Zeka') !!}</h4>
|
||||
<p class="!mb-0 flex-1 min-h-[4rem] md:min-h-[4.5rem] !text-[11px] md:!text-[14px] !leading-snug md:!leading-relaxed">{!! t('Yapay zeka teknolojilerini kullanarak işlerinizi otomatikleştirebiliriz.') !!}</p>
|
||||
</div>
|
||||
<!--/.card-body -->
|
||||
</div>
|
||||
</a>
|
||||
<!--/.card -->
|
||||
</div>
|
||||
<!--/column -->
|
||||
<div class="md:w-6/12 lg:w-6/12 xl:w-3/12 w-full flex-[0_0_auto] max-w-full !px-[15px] xl:!px-[12.5px] lg:!px-[12.5px] md:!px-[12.5px] !mt-[25px]">
|
||||
<div class="card glass-card !shadow-[0_0.25rem_1.75rem_rgba(30,34,40,0.07)] h-full flex flex-col">
|
||||
<div class="card-body flex-[1_1_auto] p-[40px] flex flex-col">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-inject icon-svg !w-[2.2rem] !h-[2.2rem] solid-mono text-[#a07cc5] text-violet !mb-3"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
|
||||
<h4>{!! t('Web Uygulamaları') !!}</h4>
|
||||
<p class="!mb-2 flex-1 min-h-[4.5rem]">{!! t('Web uygulamaları geliştirmek için gerekli olan tüm hizmetleri sunuyoruz.') !!}</p>
|
||||
<a href="{{ route('page.show', ['slug' => 'web-uygulamalari']) }}" class="more hover !text-[#a07cc5] focus:!text-[#a07cc5] hover:!text-[#a07cc5] !mt-auto">{!! t('Daha fazla bilgi edin') !!}</a>
|
||||
<div class="w-full md:w-6/12 lg:w-6/12 xl:w-3/12 flex-[0_0_auto] max-w-full glass-card-col">
|
||||
<a href="{{ route('page.show', ['slug' => 'web-uygulamalari']) }}" class="card glass-card !shadow-[0_0.25rem_1.75rem_rgba(30,34,40,0.07)] h-full flex flex-col">
|
||||
<div class="card-body glass-card-body flex-[1_1_auto] flex flex-col">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-inject icon-svg !w-[1.8rem] !h-[1.8rem] md:!w-[2.2rem] md:!h-[2.2rem] solid-mono text-[#a07cc5] text-violet !mb-2 md:!mb-3"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
|
||||
<h4 class="!text-[14px] md:!text-[1.1rem] !mb-1 md:!mb-2">{!! t('Web Uygulamaları') !!}</h4>
|
||||
<p class="!mb-0 flex-1 min-h-[4rem] md:min-h-[4.5rem] !text-[11px] md:!text-[14px] !leading-snug md:!leading-relaxed">{!! t('Web uygulamaları geliştirmek için gerekli olan tüm hizmetleri sunuyoruz.') !!}</p>
|
||||
</div>
|
||||
<!--/.card-body -->
|
||||
</div>
|
||||
</a>
|
||||
<!--/.card -->
|
||||
</div>
|
||||
<!--/column -->
|
||||
<div class="md:w-6/12 lg:w-6/12 xl:w-3/12 w-full flex-[0_0_auto] max-w-full !px-[15px] xl:!px-[12.5px] lg:!px-[12.5px] md:!px-[12.5px] !mt-[25px]">
|
||||
<div class="card glass-card !shadow-[0_0.25rem_1.75rem_rgba(30,34,40,0.07)] h-full flex flex-col">
|
||||
<div class="card-body flex-[1_1_auto] p-[40px] flex flex-col">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="svg-inject icon-svg !w-[2.2rem] !h-[2.2rem] solid-mono text-[#f78b77] text-orange !mb-3"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"></path></svg>
|
||||
<h4>{!! t('Müzik Prodüksiyon') !!}</h4>
|
||||
<p class="!mb-2 flex-1 min-h-[4.5rem]">{!! t('Müzik prodüksiyonu, film müzikleri, kurumsal müzik çalışmaları ve benzeri alanlarda hizmet veriyoruz.') !!}</p>
|
||||
<a href="{{ route('music-productions.index') }}" class="more hover !text-[#f78b77] focus:!text-[#f78b77] hover:!text-[#f78b77] !mt-auto">{!! t('Daha fazla bilgi edin') !!}</a>
|
||||
<div class="w-full md:w-6/12 lg:w-6/12 xl:w-3/12 flex-[0_0_auto] max-w-full glass-card-col">
|
||||
<a href="{{ route('music-productions.index') }}" class="card glass-card !shadow-[0_0.25rem_1.75rem_rgba(30,34,40,0.07)] h-full flex flex-col">
|
||||
<div class="card-body glass-card-body flex-[1_1_auto] flex flex-col">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="svg-inject icon-svg !w-[1.8rem] !h-[1.8rem] md:!w-[2.2rem] md:!h-[2.2rem] solid-mono text-[#f78b77] text-orange !mb-2 md:!mb-3"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"></path></svg>
|
||||
<h4 class="!text-[14px] md:!text-[1.1rem] !mb-1 md:!mb-2">{!! t('Müzik Prodüksiyon') !!}</h4>
|
||||
<p class="!mb-0 flex-1 min-h-[4rem] md:min-h-[4.5rem] !text-[11px] md:!text-[14px] !leading-snug md:!leading-relaxed">{!! t('Müzik prodüksiyonu, film müzikleri, kurumsal müzik çalışmaları ve benzeri alanlarda hizmet veriyoruz.') !!}</p>
|
||||
</div>
|
||||
<!--/.card-body -->
|
||||
</div>
|
||||
</a>
|
||||
<!--/.card -->
|
||||
</div>
|
||||
<!--/column -->
|
||||
<div class="md:w-6/12 lg:w-6/12 xl:w-3/12 w-full flex-[0_0_auto] max-w-full !px-[15px] xl:!px-[12.5px] lg:!px-[12.5px] md:!px-[12.5px] !mt-[25px]">
|
||||
<div class="card glass-card !shadow-[0_0.25rem_1.75rem_rgba(30,34,40,0.07)] h-full flex flex-col">
|
||||
<div class="card-body flex-[1_1_auto] p-[40px] flex flex-col">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-inject icon-svg !w-[2.2rem] !h-[2.2rem] solid-mono text-[#45c4a0] text-green !mb-3"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>
|
||||
<h4>{!! t('Uygulama Geliştirme') !!}</h4>
|
||||
<p class="!mb-2 flex-1 min-h-[4.5rem]">{!! t('Android, iOS, MacOS, Windows uygulamaları geliştiriyoruz.') !!}</p>
|
||||
<a href="{{ route('page.show', ['slug' => 'uygulama-gelistirme']) }}" class="more hover !text-[#45c4a0] focus:!text-[#45c4a0] hover:!text-[#45c4a0] !mt-auto">{!! t('Daha fazla bilgi edin') !!}</a>
|
||||
<div class="w-full md:w-6/12 lg:w-6/12 xl:w-3/12 flex-[0_0_auto] max-w-full glass-card-col">
|
||||
<a href="{{ route('page.show', ['slug' => 'uygulama-gelistirme']) }}" class="card glass-card !shadow-[0_0.25rem_1.75rem_rgba(30,34,40,0.07)] h-full flex flex-col">
|
||||
<div class="card-body glass-card-body flex-[1_1_auto] flex flex-col">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-inject icon-svg !w-[1.8rem] !h-[1.8rem] md:!w-[2.2rem] md:!h-[2.2rem] solid-mono text-[#45c4a0] text-green !mb-2 md:!mb-3"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>
|
||||
<h4 class="!text-[14px] md:!text-[1.1rem] !mb-1 md:!mb-2">{!! t('Uygulama Geliştirme') !!}</h4>
|
||||
<p class="!mb-0 flex-1 min-h-[4rem] md:min-h-[4.5rem] !text-[11px] md:!text-[14px] !leading-snug md:!leading-relaxed">{!! t('Android, iOS, MacOS, Windows uygulamaları geliştiriyoruz.') !!}</p>
|
||||
</div>
|
||||
<!--/.card-body -->
|
||||
</div>
|
||||
</a>
|
||||
<!--/.card -->
|
||||
</div>
|
||||
<!--/column -->
|
||||
@@ -60,35 +56,86 @@
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.glass-card-row {
|
||||
margin-left: -8px !important;
|
||||
margin-right: -8px !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.glass-card-row {
|
||||
margin-left: -12.5px !important;
|
||||
margin-right: -12.5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card-col {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
margin-top: 16px !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.glass-card-col {
|
||||
padding-left: 12.5px !important;
|
||||
padding-right: 12.5px !important;
|
||||
margin-top: 25px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card-body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.glass-card-body {
|
||||
padding: 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
position: relative !important;
|
||||
border-radius: 30px !important;
|
||||
border-radius: 16px !important;
|
||||
border: none !important;
|
||||
background: rgba(255, 255, 255, 0) !important;
|
||||
background: rgba(255, 255, 255, 0.6) !important;
|
||||
overflow: visible !important;
|
||||
isolation: isolate;
|
||||
transition: all 0.3s ease-in-out;
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.glass-card {
|
||||
border-radius: 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card svg {
|
||||
filter: brightness(1.4);
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.glass-card h4 {
|
||||
color: white;
|
||||
color: #343f52;
|
||||
transition: all 0.3s ease-in-out;
|
||||
text-shadow: 0px 1px 10px #00000080;
|
||||
}
|
||||
|
||||
.glass-card:hover h4 {
|
||||
color: black;
|
||||
color: white;
|
||||
transition: all 0.3s ease-in-out;
|
||||
text-shadow: none;
|
||||
text-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.glass-card:hover svg {
|
||||
filter: brightness(0.8);
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.glass-card p {
|
||||
color: #60697b;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.glass-card:hover p {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.glass-card::before {
|
||||
@@ -97,26 +144,39 @@
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 30px;
|
||||
border-radius: 16px;
|
||||
pointer-events: none;
|
||||
-webkit-box-shadow: inset 2px 2px 0px -2px rgba(255, 255, 255, 0.7), inset 0 0 3px 1px rgba(255, 255, 255, 0.7);
|
||||
box-shadow: inset 2px 2px 0px -2px rgba(255, 255, 255, 0.7), inset 0 0 3px 1px rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.glass-card::before {
|
||||
border-radius: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
border-radius: 30px;
|
||||
border-radius: 16px;
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
backdrop-filter: blur(15px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.glass-card::after {
|
||||
border-radius: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
transform: translateY(-5px);
|
||||
background: rgba(255, 255, 255, 0.6) !important;
|
||||
box-shadow: 0 1rem 3rem rgba(30, 34, 40, 0.12) !important;
|
||||
background: rgba(247, 108, 38, 0.85) !important;
|
||||
box-shadow: 0 1rem 3rem rgba(247, 108, 38, 0.3) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -122,8 +122,22 @@ Route::get('/stajyer/panel', [\App\Http\Controllers\CareerController::class, 'in
|
||||
Route::post('/stajyer/form-yukle', [\App\Http\Controllers\CareerController::class, 'uploadInternshipForm'])->name('intern.upload-form');
|
||||
Route::post('/stajyer/github-kaydet', [\App\Http\Controllers\CareerController::class, 'saveGithubRepo'])->name('intern.save-github');
|
||||
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::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');
|
||||
|
||||
// Intern Admin Portal
|
||||
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::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');
|
||||
|
||||
|
||||
// Payment Process
|
||||
Route::post('/payment/process', [\App\Http\Controllers\PaymentController::class, 'process'])->name('payment.process');
|
||||
Route::any('/payment/success', [\App\Http\Controllers\PaymentController::class, 'success'])->name('payment.success');
|
||||
@@ -157,8 +171,17 @@ 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');
|
||||
|
||||
// Privacy Policy Alternatives
|
||||
Route::get('/privacy-policy', function () {
|
||||
return app(PageController::class)->show('privacy');
|
||||
});
|
||||
Route::get('/gizlilik-politikasi', function () {
|
||||
return app(PageController::class)->show('privacy');
|
||||
});
|
||||
|
||||
// Nested Pages (Parent/Child)
|
||||
Route::get('/{parentSlug}/{slug}', [PageController::class, 'showNested'])->name('page.show_nested');
|
||||
|
||||
// Pages (en sonda olmalı - catch-all)
|
||||
Route::get('/{slug}', [PageController::class, 'show'])->name('page.show');
|
||||
|
||||
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
|
||||
// Tab Switch Logic
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||
const tabPanels = document.querySelectorAll('.tab-panel');
|
||||
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
// Deactivate active tab
|
||||
tabButtons.forEach(btn => {
|
||||
btn.classList.remove('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
|
||||
btn.classList.add('bg-transparent', 'border-transparent', 'text-slate-600');
|
||||
btn.querySelector('.tab-icon').classList.remove('bg-blue-600', 'text-white');
|
||||
btn.querySelector('.tab-icon').classList.add('bg-slate-50', 'text-slate-500');
|
||||
btn.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
// Hide active panels
|
||||
tabPanels.forEach(panel => {
|
||||
panel.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Activate clicked tab
|
||||
button.classList.add('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
|
||||
button.classList.remove('bg-transparent', 'border-transparent', 'text-slate-600');
|
||||
button.querySelector('.tab-icon').classList.add('bg-blue-600', 'text-white');
|
||||
button.querySelector('.tab-icon').classList.remove('bg-slate-50', 'text-slate-500');
|
||||
button.setAttribute('aria-selected', 'true');
|
||||
|
||||
// Show targets panel
|
||||
const targetId = button.getAttribute('data-tab-target');
|
||||
const targetPanel = document.getElementById(targetId + '-panel');
|
||||
if (targetPanel) {
|
||||
targetPanel.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let quill;
|
||||
|
||||
// Tab Switch Logic
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Quill editor
|
||||
quill = new Quill('#editor-content-quill', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['clean'] // remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||
const tabPanels = document.querySelectorAll('.tab-panel');
|
||||
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
// Deactivate active tab
|
||||
tabButtons.forEach(btn => {
|
||||
btn.classList.remove('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
|
||||
btn.classList.add('bg-transparent', 'border-transparent', 'text-slate-600');
|
||||
btn.querySelector('.tab-icon').classList.remove('bg-blue-600', 'text-white');
|
||||
btn.querySelector('.tab-icon').classList.add('bg-slate-50', 'text-slate-500');
|
||||
btn.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
// Hide active panels
|
||||
tabPanels.forEach(panel => {
|
||||
panel.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Activate clicked tab
|
||||
button.classList.add('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
|
||||
button.classList.remove('bg-transparent', 'border-transparent', 'text-slate-600');
|
||||
button.querySelector('.tab-icon').classList.add('bg-blue-600', 'text-white');
|
||||
button.querySelector('.tab-icon').classList.remove('bg-slate-50', 'text-slate-500');
|
||||
button.setAttribute('aria-selected', 'true');
|
||||
|
||||
// Show targets panel
|
||||
const targetId = button.getAttribute('data-tab-target');
|
||||
const targetPanel = document.getElementById(targetId + '-panel');
|
||||
if (targetPanel) {
|
||||
targetPanel.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle initial state of date logic
|
||||
calculateEndDateFrontend();
|
||||
|
||||
// Fetch Github commits in memory if repo is set
|
||||
fetchAllCommitsInMemory();
|
||||
|
||||
// Select first day of the notebook workspace on load
|
||||
selectNotebookDay(0);
|
||||
});
|
||||
|
||||
const githubRepoUrl = "dummy";
|
||||
let allGithubCommits = {};
|
||||
let activeDayIdx = 0;
|
||||
|
||||
function fetchAllCommitsInMemory() {
|
||||
if (!githubRepoUrl) return;
|
||||
|
||||
let repoClean = githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
|
||||
repoClean = repoClean.replace(/\/$/, '');
|
||||
const parts = repoClean.split('/');
|
||||
if (parts.length < 2) return;
|
||||
|
||||
const owner = parts[0];
|
||||
const repo = parts[1].replace(/\.git$/i, '');
|
||||
|
||||
fetch(`https://api.github.com/repos/${owner}/${repo}/commits?per_page=100`)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('API Error');
|
||||
return response.json();
|
||||
})
|
||||
.then(commits => {
|
||||
if (!Array.isArray(commits)) return;
|
||||
commits.forEach(item => {
|
||||
const dateStr = item.commit.author.date;
|
||||
if (dateStr) {
|
||||
const dateOnly = dateStr.substring(0, 10);
|
||||
if (!allGithubCommits[dateOnly]) {
|
||||
allGithubCommits[dateOnly] = [];
|
||||
}
|
||||
allGithubCommits[dateOnly].push(item);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn('Commits could not be pre-loaded in memory.', err);
|
||||
});
|
||||
}
|
||||
|
||||
function selectNotebookDay(idx) {
|
||||
const oldBtn = document.getElementById(`btn-day-${activeDayIdx}`);
|
||||
if (oldBtn) {
|
||||
oldBtn.classList.remove('border-blue-200', 'bg-blue-50/50');
|
||||
oldBtn.classList.add('border-slate-100', 'bg-white');
|
||||
const oldTitle = oldBtn.querySelector('span.text-xs');
|
||||
if (oldTitle) {
|
||||
oldTitle.classList.remove('text-blue-700');
|
||||
oldTitle.classList.add('text-slate-800');
|
||||
}
|
||||
}
|
||||
|
||||
activeDayIdx = idx;
|
||||
const newBtn = document.getElementById(`btn-day-${idx}`);
|
||||
if (newBtn) {
|
||||
newBtn.classList.add('border-blue-200', 'bg-blue-50/50');
|
||||
newBtn.classList.remove('border-slate-100', 'bg-white');
|
||||
const newTitle = newBtn.querySelector('span.text-xs');
|
||||
if (newTitle) {
|
||||
newTitle.classList.add('text-blue-700');
|
||||
newTitle.classList.remove('text-slate-800');
|
||||
}
|
||||
|
||||
const dayNum = newBtn.getAttribute('data-day-num');
|
||||
const dateVal = newBtn.getAttribute('data-date');
|
||||
const dateFormatted = newBtn.getAttribute('data-formatted-date');
|
||||
const savedContent = newBtn.getAttribute('data-saved-content');
|
||||
|
||||
document.getElementById('editor-day-title').textContent = `${dayNum}. Gün`;
|
||||
document.getElementById('editor-day-date').textContent = dateFormatted;
|
||||
|
||||
quill.root.innerHTML = savedContent || '';
|
||||
document.getElementById('save-status').textContent = '';
|
||||
|
||||
// Future validation
|
||||
const dateParts = dateVal.split('-');
|
||||
const selectedDate = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
|
||||
const today = new Date();
|
||||
today.setHours(0,0,0,0);
|
||||
|
||||
const isFuture = selectedDate > today;
|
||||
const warningBlock = document.getElementById('future-warning');
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const gitBtn = document.querySelector('button[onclick="fillFromGithub()"]');
|
||||
|
||||
if (isFuture) {
|
||||
warningBlock.classList.remove('hidden');
|
||||
quill.enable(false);
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
if (gitBtn) {
|
||||
gitBtn.disabled = true;
|
||||
gitBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
} else {
|
||||
warningBlock.classList.add('hidden');
|
||||
quill.enable(true);
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
if (gitBtn) {
|
||||
gitBtn.disabled = false;
|
||||
gitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillFromGithub() {
|
||||
const btn = document.getElementById(`btn-day-${activeDayIdx}`);
|
||||
if (!btn) return;
|
||||
|
||||
const dateVal = btn.getAttribute('data-date');
|
||||
const dayCommits = allGithubCommits[dateVal] || [];
|
||||
|
||||
if (dayCommits.length === 0) {
|
||||
alert("Bu staj gününe ait GitHub commit'i bulunamadı. Lütfen commit attığınızdan ve tarihin doğru olduğundan emin olun.");
|
||||
return;
|
||||
}
|
||||
|
||||
let commitHtml = "<ul>";
|
||||
const sorted = [...dayCommits].reverse();
|
||||
sorted.forEach(c => {
|
||||
const msg = c.commit.message;
|
||||
const sha = c.sha.substring(0, 7);
|
||||
const authorDate = new Date(c.commit.author.date);
|
||||
const time = authorDate.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
|
||||
commitHtml += `<li><strong>[${time}]</strong> (<em>${sha}</em>) ${msg}</li>`;
|
||||
});
|
||||
commitHtml += "</ul>";
|
||||
|
||||
const currentHtml = quill.root.innerHTML.trim();
|
||||
if (currentHtml !== "" && currentHtml !== "<p><br></p>") {
|
||||
if (confirm("Mevcut rapor içeriğinizin sonuna bu günün commitlerini eklemek ister misiniz? (Hayır derseniz mevcut içerik silinip commitler yazılır.)")) {
|
||||
quill.root.innerHTML = currentHtml + "<br><br>" + commitHtml;
|
||||
} else {
|
||||
quill.root.innerHTML = commitHtml;
|
||||
}
|
||||
} else {
|
||||
quill.root.innerHTML = commitHtml;
|
||||
}
|
||||
}
|
||||
|
||||
function saveActiveDay() {
|
||||
const btn = document.getElementById(`btn-day-${activeDayIdx}`);
|
||||
if (!btn) return;
|
||||
|
||||
const dayNum = btn.getAttribute('data-day-num');
|
||||
const dateVal = btn.getAttribute('data-date');
|
||||
let contentVal = quill.root.innerHTML;
|
||||
if (contentVal === '<p><br></p>') {
|
||||
contentVal = '';
|
||||
}
|
||||
const statusText = document.getElementById('save-status');
|
||||
|
||||
statusText.textContent = "Kaydediliyor...";
|
||||
statusText.className = "text-xs font-semibold text-slate-400 animate-pulse";
|
||||
|
||||
fetch(""dummy"", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-TOKEN": ""dummy""
|
||||
},
|
||||
body: JSON.stringify({
|
||||
day_number: dayNum,
|
||||
date: dateVal,
|
||||
content: contentVal
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
statusText.textContent = "Başarıyla kaydedildi.";
|
||||
statusText.className = "text-xs font-semibold text-green-600";
|
||||
|
||||
btn.setAttribute('data-saved-content', contentVal);
|
||||
|
||||
const badge = document.getElementById(`badge-day-${activeDayIdx}`);
|
||||
if (badge) {
|
||||
if (contentVal.trim() !== '') {
|
||||
badge.textContent = 'DOLU';
|
||||
badge.className = "inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase bg-green-50 text-green-700 border border-green-200";
|
||||
} else {
|
||||
badge.textContent = 'BOŞ';
|
||||
badge.className = "inline-flex px-2 py-0.5 rounded-lg text-[9px] font-extrabold uppercase bg-slate-50 text-slate-400 border border-slate-200";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
statusText.textContent = data.message || "Kaydedilemedi.";
|
||||
statusText.className = "text-xs font-semibold text-red-600";
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
statusText.textContent = "Bağlantı hatası oluştu.";
|
||||
statusText.className = "text-xs font-semibold text-red-600";
|
||||
});
|
||||
}
|
||||
|
||||
function openPrintModal() {
|
||||
const modal = document.getElementById('print-modal');
|
||||
const card = document.getElementById('print-modal-card');
|
||||
modal.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
card.classList.remove('scale-95', 'opacity-0');
|
||||
card.classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closePrintModal() {
|
||||
const modal = document.getElementById('print-modal');
|
||||
const card = document.getElementById('print-modal-card');
|
||||
card.classList.remove('scale-100', 'opacity-100');
|
||||
card.classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function handleFileSelected(input) {
|
||||
const fileNameElement = document.getElementById('selected-file-name');
|
||||
const submitBtn = document.getElementById('submit-upload-btn');
|
||||
if (input.files && input.files.length > 0) {
|
||||
const file = input.files[0];
|
||||
const fileSizeMB = file.size / (1024 * 1024);
|
||||
if (fileSizeMB > 50) {
|
||||
alert('Dosya boyutu 50MB\'ı aşamaz.');
|
||||
input.value = '';
|
||||
fileNameElement.textContent = '';
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
return;
|
||||
}
|
||||
fileNameElement.textContent = 'Seçilen dosya: ' + file.name + ' (' + fileSizeMB.toFixed(2) + ' MB)';
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
} else {
|
||||
fileNameElement.textContent = '';
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
}
|
||||
|
||||
function calculateEndDateFrontend() {
|
||||
const startDateVal = document.getElementById('internship_start_date').value;
|
||||
const totalDaysVal = document.getElementById('internship_total_days').value;
|
||||
const endDateInput = document.getElementById('internship_end_date');
|
||||
|
||||
if (!startDateVal || !totalDaysVal || totalDaysVal <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let date = new Date(startDateVal);
|
||||
let daysToAdd = parseInt(totalDaysVal);
|
||||
let count = 0;
|
||||
let endDate = new Date(startDateVal);
|
||||
|
||||
while (count < daysToAdd) {
|
||||
const dayOfWeek = date.getDay();
|
||||
if (dayOfWeek === 6 || dayOfWeek === 0) { // 6 = Saturday, 0 = Sunday
|
||||
date.setDate(date.getDate() + 1);
|
||||
continue;
|
||||
}
|
||||
endDate = new Date(date);
|
||||
date.setDate(date.getDate() + 1);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Format to YYYY-MM-DD
|
||||
const yyyy = endDate.getFullYear();
|
||||
const mm = String(endDate.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(endDate.getDate()).padStart(2, '0');
|
||||
endDateInput.value = `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
function copyToClipboard(elementId, btn) {
|
||||
const text = document.getElementById(elementId).textContent;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const icon = btn.querySelector('i');
|
||||
const originalClass = icon.className;
|
||||
icon.className = 'uil uil-check text-green-500';
|
||||
setTimeout(() => {
|
||||
icon.className = originalClass;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user