feat: implement comprehensive project management and client tracking portal with Filament integration

This commit is contained in:
Ümit Tunç
2026-07-30 17:25:05 +03:00
parent 4576739e6e
commit 26cab615f1
16 changed files with 1558 additions and 0 deletions
@@ -0,0 +1,27 @@
<?php
namespace App\Filament\Admin\Resources\Projects\Pages;
use App\Filament\Admin\Resources\Projects\ProjectResource;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\Width;
class CreateProject extends CreateRecord
{
protected static string $resource = ProjectResource::class;
public function getTitle(): string
{
return 'Yeni Proje Oluştur';
}
public function getMaxContentWidth(): Width | string | null
{
return Width::Full;
}
protected function afterCreate(): void
{
$this->record->recalculateProgress();
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Filament\Admin\Resources\Projects\Pages;
use App\Filament\Admin\Resources\Projects\ProjectResource;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Width;
class EditProject extends EditRecord
{
protected static string $resource = ProjectResource::class;
public function getTitle(): string
{
return 'Proje Yönetimi: ' . $this->record->title;
}
public function getMaxContentWidth(): Width | string | null
{
return Width::Full;
}
protected function getHeaderActions(): array
{
return [
Action::make('recalculate')
->label('İlerlemeyi Yeniden Hesapla')
->icon('heroicon-o-calculator')
->color('info')
->action(function () {
$pct = $this->record->recalculateProgress();
Notification::make()
->title('Proje ilerleme yüzdesi güncellendi: %' . $pct)
->success()
->send();
}),
Action::make('preview_portal')
->label('Müşteri Ekranında Gör')
->icon('heroicon-o-eye')
->color('warning')
->url(fn () => route('projects.show', $this->record->slug))
->openUrlInNewTab(),
DeleteAction::make(),
];
}
protected function afterSave(): void
{
$this->record->recalculateProgress();
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Admin\Resources\Projects\Pages;
use App\Filament\Admin\Resources\Projects\ProjectResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListProjects extends ListRecords
{
protected static string $resource = ProjectResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Yeni Proje Başlat'),
];
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Filament\Admin\Resources\Projects;
use App\Filament\Admin\Resources\Projects\Pages\CreateProject;
use App\Filament\Admin\Resources\Projects\Pages\EditProject;
use App\Filament\Admin\Resources\Projects\Pages\ListProjects;
use App\Filament\Admin\Resources\Projects\Schemas\ProjectForm;
use App\Filament\Admin\Resources\Projects\Tables\ProjectsTable;
use App\Models\Project;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class ProjectResource extends Resource
{
protected static ?string $model = Project::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
public static function getNavigationLabel(): string
{
return 'Proje Takip';
}
public static function getModelLabel(): string
{
return 'Proje';
}
public static function getPluralModelLabel(): string
{
return 'Proje Yönetimi';
}
public static function form(Schema $schema): Schema
{
return ProjectForm::configure($schema);
}
public static function table(Table $table): Table
{
return ProjectsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListProjects::route('/'),
'create' => CreateProject::route('/create'),
'edit' => EditProject::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}
@@ -0,0 +1,259 @@
<?php
namespace App\Filament\Admin\Resources\Projects\Schemas;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Schema;
use Illuminate\Support\Str;
class ProjectForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->schema([
Tabs::make('ProjectTabs')
->tabs([
Tab::make('Proje Detayları & Müşteri')
->icon('heroicon-m-briefcase')
->schema([
Section::make('Temel Proje Bilgileri')
->columns(12)
->schema([
TextInput::make('title')
->label('Proje Adı')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (string $operation, $state, callable $set) {
if ($operation !== 'create') return;
$set('slug', Str::slug($state));
})
->columnSpan(6),
TextInput::make('slug')
->label('URL Slug')
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->rules(['alpha_dash'])
->columnSpan(6),
TextInput::make('client_name')
->label('Müşteri Unvanı / Firma')
->required()
->maxLength(255)
->columnSpan(4),
TextInput::make('client_email')
->label('Müşteri E-Posta')
->email()
->maxLength(255)
->columnSpan(4),
TextInput::make('client_access_code')
->label('Müşteri Giriş Şifresi / PIN')
->default(fn () => strtoupper(Str::random(6)))
->helperText('Müşterinin takip ekranına giriş yapabileceği şifre')
->columnSpan(4),
Select::make('proposal_id')
->label('İlişkili Fiyat Teklifi')
->relationship('proposal', 'title')
->searchable()
->preload()
->nullable()
->live()
->afterStateUpdated(function ($state, callable $set) {
if ($state) {
$prop = \App\Models\Proposal::find($state);
if ($prop) {
$set('slug', $prop->slug);
}
}
})
->columnSpan(4),
Select::make('status')
->label('Proje Genel Durumu')
->options([
'planning' => 'Planlama Aşamasında',
'in_progress' => 'Devam Ediyor (Aktif)',
'on_hold' => 'Beklemeye Alındı',
'completed' => 'Tamamlandı',
'cancelled' => 'İptal Edildi',
])
->default('in_progress')
->required()
->columnSpan(4),
TextInput::make('progress_percent')
->label('İlerleme Yüzdesi (%)')
->numeric()
->default(0)
->suffix('%')
->columnSpan(4),
DatePicker::make('start_date')
->label('Proje Başlangıç Tarihi')
->native(false)
->displayFormat('d.m.Y')
->columnSpan(6),
DatePicker::make('target_date')
->label('Hedef Bitiş Tarihi')
->native(false)
->displayFormat('d.m.Y')
->columnSpan(6),
Textarea::make('notes')
->label('İç Notlar ve Açıklamalar')
->rows(3)
->columnSpanFull(),
]),
]),
Tab::make('Hizmet Modülleri (% İlerleme)')
->icon('heroicon-m-squares-2x2')
->schema([
Section::make('Sözleşme Kapsamındaki Modüller')
->description('Hangi modüllerin tamamlandığını işaretleyin. İlerleme yüzdesi modül ağırlıklarına göre otomatik hesaplanır.')
->schema([
Repeater::make('modules')
->relationship('modules')
->columns(12)
->orderColumn('order')
->defaultItems(0)
->schema([
TextInput::make('title')
->label('Modül Adı')
->required()
->columnSpan(4),
TextInput::make('weight_percent')
->label('Ağırlık (%)')
->numeric()
->default(10)
->suffix('%')
->columnSpan(2),
Select::make('status')
->label('Durum')
->options([
'pending' => 'Bekliyor',
'in_progress' => 'Devam Ediyor',
'completed' => 'Tamamlandı',
])
->default('pending')
->required()
->columnSpan(3),
DatePicker::make('start_date')
->label('Başlangıç')
->native(false)
->columnSpan(1.5),
DatePicker::make('end_date')
->label('Bitiş')
->native(false)
->columnSpan(1.5),
])
->columnSpanFull(),
]),
]),
Tab::make('Kanban Görev Kartları')
->icon('heroicon-m-view-columns')
->schema([
Section::make('İş Takvimi ve Görev Listesi')
->description('Sözleşmedeki taskları ve detaylı görev kartlarını burada yönetin.')
->schema([
Repeater::make('tasks')
->relationship('tasks')
->columns(12)
->orderColumn('order_index')
->defaultItems(0)
->schema([
TextInput::make('title')
->label('Görev Başlığı')
->required()
->columnSpan(4),
Select::make('status')
->label('Kanban Durumu')
->options([
'todo' => 'Yapılacak',
'in_progress' => 'Devam Ediyor',
'review' => 'Kontrol Bekliyor',
'done' => 'Tamamlandı',
])
->default('todo')
->required()
->columnSpan(3),
Select::make('priority')
->label('Öncelik')
->options([
'low' => 'Düşük',
'medium' => 'Orta',
'high' => 'Yüksek',
'urgent' => 'Acil',
])
->default('medium')
->columnSpan(2),
TextInput::make('assigned_person')
->label('Sorumlu')
->columnSpan(3),
])
->columnSpanFull(),
]),
]),
Tab::make('Gidişat Güncellemeleri Logu')
->icon('heroicon-m-chat-bubble-bottom-center-text')
->schema([
Section::make('Canlı İlerleme Duyuruları')
->description('Yazılım ekibi tarafından girilen ve müşterinin canlı izleyebildiği durum mesajları.')
->schema([
Repeater::make('updates')
->relationship('updates')
->columns(12)
->defaultItems(0)
->schema([
TextInput::make('title')
->label('Güncelleme Başlığı')
->required()
->columnSpan(6),
TextInput::make('progress_percent_at_update')
->label('O Anki Yüzde (%)')
->numeric()
->columnSpan(3),
Checkbox::make('is_public')
->label('Müşteriye Göster')
->default(true)
->columnSpan(3),
Textarea::make('content')
->label('Güncelleme Notu / Sürüm Açıklaması')
->required()
->rows(2)
->columnSpanFull(),
])
->columnSpanFull(),
]),
]),
])->columnSpanFull()
]);
}
}
@@ -0,0 +1,110 @@
<?php
namespace App\Filament\Admin\Resources\Projects\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
class ProjectsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->label('Proje Adı')
->searchable()
->sortable()
->limit(40),
TextColumn::make('client_name')
->label('Müşteri')
->searchable()
->sortable()
->limit(30),
TextColumn::make('progress_percent')
->label('İlerleme')
->formatStateUsing(fn ($state) => "%{$state}")
->badge()
->color(fn ($state) => match (true) {
$state >= 100 => 'success',
$state >= 50 => 'info',
$state >= 25 => 'warning',
default => 'danger',
})
->sortable(),
TextColumn::make('status')
->label('Durum')
->badge()
->color(fn (string $state): string => match ($state) {
'planning' => 'gray',
'in_progress' => 'info',
'on_hold' => 'warning',
'completed' => 'success',
'cancelled' => 'danger',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => match ($state) {
'planning' => 'Planlama',
'in_progress' => 'Devam Ediyor',
'on_hold' => 'Beklemede',
'completed' => 'Tamamlandı',
'cancelled' => 'İptal',
default => $state,
}),
TextColumn::make('client_access_code')
->label('Müşteri PIN')
->copyable()
->badge()
->color('gray'),
TextColumn::make('target_date')
->label('Hedef Bitiş')
->date('d.m.Y')
->sortable(),
TextColumn::make('created_at')
->label('Oluşturulma')
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
SelectFilter::make('status')
->label('Durum')
->options([
'planning' => 'Planlama',
'in_progress' => 'Devam Ediyor',
'on_hold' => 'Beklemede',
'completed' => 'Tamamlandı',
'cancelled' => 'İptal',
]),
])
->recordActions([
EditAction::make()
->label('Düzenle'),
])
->actions([
Action::make('view_client_portal')
->label('Müşteri Ekranı')
->icon('heroicon-o-arrow-top-right-on-square')
->url(fn ($record) => route('projects.show', $record->slug))
->openUrlInNewTab(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->label('Sil'),
]),
])
->defaultSort('created_at', 'desc');
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers;
use App\Models\Project;
use Illuminate\Http\Request;
class ProjectController extends Controller
{
/**
* Display the public/client project tracking portal.
*/
public function show(Request $request, string $slug)
{
$project = Project::where('slug', $slug)
->with(['modules', 'tasks', 'updates' => function($q) {
$q->where('is_public', true)->latest();
}, 'proposal'])
->firstOrFail();
// Recalculate progress dynamically
$project->recalculateProgress();
// Check if access code protection is active and verified in session
$sessionKey = 'project_access_' . $project->id;
$isVerified = session()->get($sessionKey, true); // Verified by default for link convenience
return view('projects.show', compact('project', 'isVerified'));
}
/**
* Verify client access code (PIN).
*/
public function verify(Request $request, string $slug)
{
$project = Project::where('slug', $slug)->firstOrFail();
$request->validate([
'access_code' => 'required|string',
]);
if (strtoupper(trim($request->input('access_code'))) === strtoupper($project->client_access_code)) {
session()->put('project_access_' . $project->id, true);
return back()->with('success', 'Erişim doğrulandı.');
}
return back()->withErrors(['access_code' => 'Geçersiz müşteri takip şifresi.']);
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class Project extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'proposal_id',
'title',
'slug',
'client_name',
'client_email',
'client_access_code',
'status',
'progress_percent',
'start_date',
'target_date',
'completed_at',
'notes',
];
protected $casts = [
'start_date' => 'date',
'target_date' => 'date',
'completed_at' => 'datetime',
'progress_percent' => 'integer',
];
protected static function boot()
{
parent::boot();
static::creating(function ($project) {
if (empty($project->slug)) {
$project->slug = Str::slug($project->title) . '-' . Str::random(5);
}
if (empty($project->client_access_code)) {
$project->client_access_code = strtoupper(Str::random(6));
}
});
}
public function proposal()
{
return $this->belongsTo(Proposal::class);
}
public function modules()
{
return $this->hasMany(ProjectModule::class)->orderBy('order', 'asc');
}
public function tasks()
{
return $this->hasMany(ProjectTask::class)->orderBy('order_index', 'asc');
}
public function updates()
{
return $this->hasMany(ProjectUpdate::class)->latest();
}
/**
* Recalculate progress percentage based on completed modules weight
*/
public function recalculateProgress()
{
$totalWeight = $this->modules()->sum('weight_percent');
if ($totalWeight > 0) {
$completedWeight = $this->modules()->where('status', 'completed')->sum('weight_percent');
$progress = (int) round(($completedWeight / $totalWeight) * 100);
} else {
$totalTasks = $this->tasks()->count();
if ($totalTasks > 0) {
$completedTasks = $this->tasks()->where('status', 'done')->count();
$progress = (int) round(($completedTasks / $totalTasks) * 100);
} else {
$progress = $this->progress_percent;
}
}
$this->update(['progress_percent' => min(100, max(0, $progress))]);
return $this->progress_percent;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectModule extends Model
{
use HasFactory;
protected $fillable = [
'project_id',
'title',
'description',
'weight_percent',
'status',
'start_date',
'end_date',
'order',
];
protected $casts = [
'start_date' => 'date',
'end_date' => 'date',
'weight_percent' => 'integer',
'order' => 'integer',
];
public function project()
{
return $this->belongsTo(Project::class);
}
public function tasks()
{
return $this->hasMany(ProjectTask::class, 'project_module_id');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectTask extends Model
{
use HasFactory;
protected $fillable = [
'project_id',
'project_module_id',
'title',
'description',
'status',
'priority',
'due_date',
'assigned_person',
'order_index',
];
protected $casts = [
'due_date' => 'date',
'order_index' => 'integer',
];
public function project()
{
return $this->belongsTo(Project::class);
}
public function module()
{
return $this->belongsTo(ProjectModule::class, 'project_module_id');
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectUpdate extends Model
{
use HasFactory;
protected $fillable = [
'project_id',
'user_id',
'title',
'content',
'progress_percent_at_update',
'is_public',
];
protected $casts = [
'progress_percent_at_update' => 'integer',
'is_public' => 'boolean',
];
public function project()
{
return $this->belongsTo(Project::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
@@ -0,0 +1,81 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->foreignId('proposal_id')->nullable()->constrained('proposals')->nullOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->string('client_name');
$table->string('client_email')->nullable();
$table->string('client_access_code')->nullable();
$table->enum('status', ['planning', 'in_progress', 'on_hold', 'completed', 'cancelled'])->default('in_progress');
$table->unsignedInteger('progress_percent')->default(0);
$table->date('start_date')->nullable();
$table->date('target_date')->nullable();
$table->timestamp('completed_at')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('project_modules', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->string('title');
$table->text('description')->nullable();
$table->unsignedInteger('weight_percent')->default(10);
$table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
$table->date('start_date')->nullable();
$table->date('end_date')->nullable();
$table->integer('order')->default(0);
$table->timestamps();
});
Schema::create('project_tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->foreignId('project_module_id')->nullable()->constrained('project_modules')->nullOnDelete();
$table->string('title');
$table->text('description')->nullable();
$table->enum('status', ['todo', 'in_progress', 'review', 'done'])->default('todo');
$table->enum('priority', ['low', 'medium', 'high', 'urgent'])->default('medium');
$table->date('due_date')->nullable();
$table->string('assigned_person')->nullable();
$table->integer('order_index')->default(0);
$table->timestamps();
});
Schema::create('project_updates', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('title');
$table->text('content');
$table->unsignedInteger('progress_percent_at_update')->nullable();
$table->boolean('is_public')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_updates');
Schema::dropIfExists('project_tasks');
Schema::dropIfExists('project_modules');
Schema::dropIfExists('projects');
}
};
+2
View File
@@ -2,5 +2,7 @@ User-agent: *
Allow: / Allow: /
Disallow: /admin/ Disallow: /admin/
Disallow: /stajyer/admin/ Disallow: /stajyer/admin/
Disallow: /teklif/
Disallow: /proje-takip/
Sitemap: https://truncgil.com/sitemap.xml Sitemap: https://truncgil.com/sitemap.xml
+666
View File
@@ -0,0 +1,666 @@
<!DOCTYPE html>
<html lang="tr" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="robots" content="noindex, nofollow">
<title>{{ $project->title }} - Canlı Proje Takip Portalı | Trunçgil Teknoloji</title>
<!-- Google Fonts: Inter & Outfit -->
<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=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Outfit', 'sans-serif'],
},
colors: {
brand: {
50: '#fff7ed',
100: '#ffedd5',
500: '#f97316',
600: '#ea580c',
700: '#c2410c',
}
}
}
}
}
</script>
<style type="text/css">
.glass-header {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.dark .glass-header {
background: rgba(15, 23, 42, 0.8);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.glass-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(8px);
border: 1px solid rgba(0, 0, 0, 0.06);
}
.dark .glass-card {
background: rgba(30, 41, 59, 0.7);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.mermaid-container {
background: #ffffff;
border-radius: 16px;
margin: 1.5rem 0;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05);
overflow: hidden;
position: relative;
}
.dark .mermaid-container {
background: #0f172a;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.mermaid-toolbar {
display: flex;
align-items: center;
padding: 10px 16px;
background: rgba(248, 250, 252, 0.9);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.dark .mermaid-toolbar {
background: rgba(15, 23, 42, 0.9);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.mermaid-canvas {
width: 100%;
height: 480px;
position: relative;
overflow: hidden;
cursor: grab;
}
.mermaid-canvas:active { cursor: grabbing; }
/* Mermaid Theme Customization */
.mermaid svg .task {
fill: #ea580c !important;
stroke: #c2410c !important;
rx: 6px !important;
ry: 6px !important;
}
.mermaid svg .task0, .mermaid svg .task2 {
fill: #ea580c !important;
stroke: #c2410c !important;
}
.mermaid svg .task1, .mermaid svg .task3 {
fill: #ef4444 !important;
stroke: #b91c1c !important;
}
.mermaid svg .taskText {
fill: #ffffff !important;
font-family: 'Inter', sans-serif !important;
font-weight: 600 !important;
}
.mermaid svg .section0, .mermaid svg .section2 {
fill: rgba(234, 88, 12, 0.08) !important;
}
.dark .mermaid svg .section0, .dark .mermaid svg .section2 {
fill: rgba(234, 88, 12, 0.18) !important;
}
.mermaid svg .section1, .mermaid svg .section3 {
fill: rgba(239, 68, 68, 0.08) !important;
}
.dark .mermaid svg .section1, .dark .mermaid svg .section3 {
fill: rgba(239, 68, 68, 0.18) !important;
}
</style>
</head>
<body class="bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 min-h-screen font-sans transition-colors duration-300">
<!-- Header Navigation -->
<header class="sticky top-0 z-40 w-full glass-header">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-18 flex items-center justify-between py-4">
<!-- Logos -->
<div class="flex items-center gap-3">
<a href="https://www.truncgil.com.tr" target="_blank" class="flex items-center gap-2">
<img src="{{ asset('logos/truncgil-yatay.svg') }}" alt="Trunçgil Teknoloji" class="h-8 w-auto dark:hidden">
<img src="{{ asset('logos/truncgil-yatay-dark.svg') }}" alt="Trunçgil Teknoloji" class="h-8 w-auto hidden dark:block">
</a>
<div class="h-6 w-px bg-slate-300 dark:bg-slate-700"></div>
<div class="flex flex-col">
<span class="text-xs font-bold uppercase tracking-wider text-orange-600 dark:text-orange-400">Canlı Proje Takip Portalı</span>
<span class="text-sm font-semibold text-slate-700 dark:text-slate-200 line-clamp-1">{{ $project->client_name }}</span>
</div>
</div>
<!-- Right Controls -->
<div class="flex items-center gap-3">
@if($project->proposal)
<a href="{{ route('proposals.show', $project->proposal->slug) }}" target="_blank" class="px-3.5 py-1.5 rounded-xl bg-orange-50 dark:bg-orange-950/40 text-orange-600 dark:text-orange-400 border border-orange-200 dark:border-orange-800/40 text-xs font-bold hover:bg-orange-100 transition-all flex items-center gap-1.5">
<i data-lucide="file-text" class="w-3.5 h-3.5"></i>
<span>Sözleşme / Teklif</span>
</a>
@endif
<button id="theme-toggle" class="p-2 rounded-xl border border-slate-200 dark:border-slate-800 hover:bg-slate-100 dark:hover:bg-slate-800 transition-all">
<i data-lucide="sun" id="theme-toggle-light-icon" class="w-4 h-4 text-amber-500 hidden"></i>
<i data-lucide="moon" id="theme-toggle-dark-icon" class="w-4 h-4 text-slate-600 hidden"></i>
</button>
</div>
</div>
</header>
<!-- Main Container -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
<!-- Hero Section & Progress Banner -->
<div class="relative overflow-hidden rounded-3xl bg-gradient-to-br from-slate-900 via-slate-800 to-orange-950 text-white p-6 sm:p-10 shadow-2xl border border-slate-800">
<div class="absolute -top-24 -right-24 w-96 h-96 bg-orange-600/20 rounded-full blur-3xl pointer-events-none"></div>
<div class="relative z-10 grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
<!-- Left: Title & Info -->
<div class="lg:col-span-8 space-y-4">
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-orange-500/20 border border-orange-500/30 text-orange-300 text-xs font-bold uppercase tracking-wider">
<span class="w-2 h-2 rounded-full bg-orange-400 animate-ping"></span>
<span>MÜŞTERİ CANLI TAKİP PANELİ</span>
</div>
<h1 class="text-2xl sm:text-4xl font-extrabold font-display leading-tight text-white">
{{ $project->title }}
</h1>
<p class="text-sm sm:text-base text-slate-300 font-medium">
Bu ekran, <strong>{{ $project->client_name }}</strong> projesine ait iş takvimini, tamamlanan hizmet modüllerini ve canlı Kanban görev akışını şeffaf biçimde sunmaktadır.
</p>
<!-- Meta Tags -->
<div class="flex flex-wrap items-center gap-4 text-xs font-semibold pt-2 text-slate-300">
@if($project->start_date)
<div class="flex items-center gap-1.5 bg-slate-800/80 px-3 py-1.5 rounded-xl border border-slate-700">
<i data-lucide="calendar" class="w-3.5 h-3.5 text-orange-400"></i>
<span>Başlangıç: {{ $project->start_date->format('d.m.Y') }}</span>
</div>
@endif
@if($project->target_date)
<div class="flex items-center gap-1.5 bg-slate-800/80 px-3 py-1.5 rounded-xl border border-slate-700">
<i data-lucide="flag" class="w-3.5 h-3.5 text-rose-400"></i>
<span>Hedef Bitiş: {{ $project->target_date->format('d.m.Y') }}</span>
</div>
@endif
<div class="flex items-center gap-1.5 bg-orange-500/20 px-3 py-1.5 rounded-xl border border-orange-500/40 text-orange-300">
<i data-lucide="key" class="w-3.5 h-3.5"></i>
<span>Müşteri PIN: <strong>{{ $project->client_access_code }}</strong></span>
</div>
</div>
</div>
<!-- Right: Progress Meter -->
<div class="lg:col-span-4 flex flex-col items-center justify-center p-6 bg-slate-800/60 backdrop-blur-md rounded-2xl border border-slate-700/60 shadow-inner">
<span class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">GENEL PROJE İLERLEMESİ</span>
<!-- Progress Donut/Bar -->
<div class="text-5xl font-extrabold font-display text-transparent bg-clip-text bg-gradient-to-r from-orange-400 to-rose-400 mb-3">
%{{ $project->progress_percent }}
</div>
<div class="w-full bg-slate-700 h-3 rounded-full overflow-hidden mb-3">
<div class="bg-gradient-to-r from-orange-500 to-rose-500 h-full rounded-full transition-all duration-1000 shadow-lg shadow-orange-500/50" style="width: {{ $project->progress_percent }}%"></div>
</div>
<span class="text-xs text-slate-300 font-medium text-center">
@if($project->progress_percent >= 100)
🎉 Proje %100 Başarıyla Tamamlandı!
@elseif($project->progress_percent >= 50)
⚡ Proje Geliştirmeleri Hızla Devam Ediyor
@else
🚀 Faz 1 Çalışmaları Başlatıldı
@endif
</span>
</div>
</div>
</div>
<!-- Interactive Section Tabs -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<a href="#gantt-section" class="glass-card p-4 rounded-2xl hover:border-orange-500/50 transition-all flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-orange-500/10 text-orange-600 dark:text-orange-400 flex items-center justify-center font-bold">
<i data-lucide="gantt-chart-square" class="w-5 h-5"></i>
</div>
<div>
<h4 class="text-sm font-bold text-slate-900 dark:text-white">İş Takvimi (Gantt)</h4>
<p class="text-xs text-slate-500">12 Haftalık Zaman Çizelgesi</p>
</div>
</a>
<a href="#modules-section" class="glass-card p-4 rounded-2xl hover:border-orange-500/50 transition-all flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-rose-500/10 text-rose-600 dark:text-rose-400 flex items-center justify-center font-bold">
<i data-lucide="layers" class="w-5 h-5"></i>
</div>
<div>
<h4 class="text-sm font-bold text-slate-900 dark:text-white">Hizmet Modülleri</h4>
<p class="text-xs text-slate-500">{{ $project->modules->where('status', 'completed')->count() }} / {{ $project->modules->count() }} Modül Bitti</p>
</div>
</a>
<a href="#kanban-section" class="glass-card p-4 rounded-2xl hover:border-orange-500/50 transition-all flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center font-bold">
<i data-lucide="kanban-square" class="w-5 h-5"></i>
</div>
<div>
<h4 class="text-sm font-bold text-slate-900 dark:text-white">Kanban Panosu</h4>
<p class="text-xs text-slate-500">{{ $project->tasks->count() }} Adet Görev Kartı</p>
</div>
</a>
<a href="#updates-section" class="glass-card p-4 rounded-2xl hover:border-orange-500/50 transition-all flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center font-bold">
<i data-lucide="activity" class="w-5 h-5"></i>
</div>
<div>
<h4 class="text-sm font-bold text-slate-900 dark:text-white">Canlı Log Akışı</h4>
<p class="text-xs text-slate-500">{{ $project->updates->count() }} İlerleme Duyurusu</p>
</div>
</a>
</div>
<!-- Section 1: Gantt Chart -->
<section id="gantt-section" class="glass-card rounded-3xl p-6 sm:p-8 space-y-4">
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-xl bg-orange-600 text-white flex items-center justify-center font-bold">
<i data-lucide="calendar" class="w-4 h-4"></i>
</div>
<div>
<h2 class="text-lg font-bold text-slate-900 dark:text-white font-display">1. İş Takvimi ve Gantt Çizelgesi</h2>
<p class="text-xs text-slate-500">Proje aşamalarının ve geliştirme süreçlerinin zamansal planı</p>
</div>
</div>
</div>
<!-- Mermaid Gantt Container -->
<div class="mermaid-container no-print">
<div class="mermaid-toolbar">
<span class="text-xs font-bold uppercase tracking-wider text-slate-400">GANTT ZAMAN ÇİZELGESİ</span>
<div class="ml-auto flex items-center gap-2">
<button onclick="zoomGantt('out')" class="p-1.5 rounded-lg border border-slate-200 dark:border-slate-800 hover:bg-slate-100 dark:hover:bg-slate-800 text-xs font-bold">Zoom -</button>
<span id="gantt-zoom-pct" class="text-xs font-mono text-slate-500">100%</span>
<button onclick="zoomGantt('in')" class="p-1.5 rounded-lg border border-slate-200 dark:border-slate-800 hover:bg-slate-100 dark:hover:bg-slate-800 text-xs font-bold">Zoom +</button>
</div>
</div>
<div class="mermaid-canvas" id="gantt-canvas">
<div class="mermaid w-full h-full">
gantt
title {{ $project->title }} (İş Takvimi)
dateFormat YYYY-MM-DD
section Faz 1: İş Takip & WhatsApp Otomasyonu
Backend REST API & Veritabanı :a1, 2026-08-01, 21d
Flutter Responsive Kanban Board :a2, after a1, 21d
WhatsApp API & AI NLP Parser :a3, after a2, 14d
AI Görsel Analiz & PDF Raporlama :a4, after a3, 14d
section Faz 2: CMS Migrasyonu & Web Sync
Citrus CMS Migrasyonu & Formlar :b1, 2026-10-10, 7d
Portfolyo Web Sync & Nihai Testler :b2, after b1, 7d
</div>
</div>
</div>
</section>
<!-- Section 2: Modules Completion Matrix -->
<section id="modules-section" class="glass-card rounded-3xl p-6 sm:p-8 space-y-6">
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-xl bg-rose-600 text-white flex items-center justify-center font-bold">
<i data-lucide="check-square" class="w-4 h-4"></i>
</div>
<div>
<h2 class="text-lg font-bold text-slate-900 dark:text-white font-display">2. Hizmet Modülleri İlerleme Matrisi</h2>
<p class="text-xs text-slate-500">Sözleşmedeki hizmet kalemlerinin tamamlanma ve gidişat durumları</p>
</div>
</div>
<span class="text-xs font-bold px-3 py-1 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">
Toplam {{ $project->modules->count() }} Modül
</span>
</div>
<!-- Modules Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@forelse($project->modules as $module)
<div class="p-5 rounded-2xl border transition-all duration-300 {{ $module->status === 'completed' ? 'bg-emerald-50/40 dark:bg-emerald-950/20 border-emerald-500/30' : ($module->status === 'in_progress' ? 'bg-orange-50/40 dark:bg-orange-950/20 border-orange-500/30' : 'bg-slate-100/50 dark:bg-slate-800/40 border-slate-200 dark:border-slate-800') }}">
<div class="flex items-start justify-between gap-3 mb-3">
<div class="flex items-center gap-2.5">
@if($module->status === 'completed')
<div class="w-6 h-6 rounded-full bg-emerald-500 text-white flex items-center justify-center flex-shrink-0">
<i data-lucide="check" class="w-3.5 h-3.5"></i>
</div>
@elseif($module->status === 'in_progress')
<div class="w-6 h-6 rounded-full bg-orange-500 text-white flex items-center justify-center flex-shrink-0 animate-pulse">
<i data-lucide="play" class="w-3 h-3"></i>
</div>
@else
<div class="w-6 h-6 rounded-full bg-slate-300 dark:bg-slate-700 text-slate-600 dark:text-slate-400 flex items-center justify-center flex-shrink-0">
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
</div>
@endif
<h3 class="font-bold text-slate-900 dark:text-white text-base">
{{ $module->title }}
</h3>
</div>
<span class="text-xs font-extrabold px-2.5 py-1 rounded-lg {{ $module->status === 'completed' ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' : ($module->status === 'in_progress' ? 'bg-orange-500/10 text-orange-600 dark:text-orange-400' : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-400') }}">
@if($module->status === 'completed') TAMAMLANDI @elseif($module->status === 'in_progress') DEVAM EDİYOR @else BEKLİYOR @endif
</span>
</div>
@if($module->description)
<p class="text-xs text-slate-600 dark:text-slate-400 leading-relaxed mb-3 pl-8">
{{ $module->description }}
</p>
@endif
<div class="flex items-center justify-between text-xs font-semibold pt-2 border-t border-slate-200/60 dark:border-slate-800/60 pl-8">
<span class="text-slate-500">Ağırlık: %{{ $module->weight_percent }}</span>
@if($module->start_date && $module->end_date)
<span class="text-slate-400">{{ $module->start_date->format('d.m') }} - {{ $module->end_date->format('d.m.Y') }}</span>
@endif
</div>
</div>
@empty
<div class="col-span-full text-center py-8 text-slate-400 text-sm">
Henüz tanımlanmış modül bulunmamaktadır.
</div>
@endforelse
</div>
</section>
<!-- Section 3: Live Kanban Board -->
<section id="kanban-section" class="glass-card rounded-3xl p-6 sm:p-8 space-y-6">
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-xl bg-amber-600 text-white flex items-center justify-center font-bold">
<i data-lucide="kanban-square" class="w-4 h-4"></i>
</div>
<div>
<h2 class="text-lg font-bold text-slate-900 dark:text-white font-display">3. Canlı Kanban Görev Panosu</h2>
<p class="text-xs text-slate-500">Mühendislik ekibinin anlık görev statüleri ve çalışma kartları</p>
</div>
</div>
</div>
<!-- 4 Column Kanban Board -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Column 1: To Do -->
<div class="bg-slate-100/70 dark:bg-slate-800/50 rounded-2xl p-4 space-y-3">
<div class="flex items-center justify-between pb-2 border-b border-slate-200 dark:border-slate-700">
<span class="text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-slate-400"></span> Yapılacaklar
</span>
<span class="text-xs font-extrabold px-2 py-0.5 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300">
{{ $project->tasks->where('status', 'todo')->count() }}
</span>
</div>
@forelse($project->tasks->where('status', 'todo') as $task)
<div class="p-3.5 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm space-y-2">
<h4 class="text-xs font-bold text-slate-900 dark:text-white">{{ $task->title }}</h4>
@if($task->description)
<p class="text-[11px] text-slate-500 line-clamp-2">{{ $task->description }}</p>
@endif
<div class="flex items-center justify-between text-[10px] font-semibold text-slate-400 pt-1">
<span>{{ $task->assigned_person ?? 'Atanmadı' }}</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 uppercase">{{ $task->priority }}</span>
</div>
</div>
@empty
<p class="text-xs text-slate-400 italic text-center py-4">Görev yok</p>
@endforelse
</div>
<!-- Column 2: In Progress -->
<div class="bg-orange-50/60 dark:bg-orange-950/20 rounded-2xl p-4 space-y-3 border border-orange-500/20">
<div class="flex items-center justify-between pb-2 border-b border-orange-200 dark:border-orange-800/40">
<span class="text-xs font-bold uppercase tracking-wider text-orange-600 dark:text-orange-400 flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-orange-500 animate-ping"></span> Devam Edenler
</span>
<span class="text-xs font-extrabold px-2 py-0.5 rounded-full bg-orange-200 dark:bg-orange-900/60 text-orange-700 dark:text-orange-300">
{{ $project->tasks->where('status', 'in_progress')->count() }}
</span>
</div>
@forelse($project->tasks->where('status', 'in_progress') as $task)
<div class="p-3.5 bg-white dark:bg-slate-900 rounded-xl border border-orange-200 dark:border-orange-900/40 shadow-sm space-y-2">
<h4 class="text-xs font-bold text-slate-900 dark:text-white">{{ $task->title }}</h4>
@if($task->description)
<p class="text-[11px] text-slate-500 line-clamp-2">{{ $task->description }}</p>
@endif
<div class="flex items-center justify-between text-[10px] font-semibold text-orange-600 dark:text-orange-400 pt-1">
<span>{{ $task->assigned_person ?? 'Yazılım Ekibi' }}</span>
<span class="px-2 py-0.5 rounded bg-orange-100 dark:bg-orange-950 uppercase">{{ $task->priority }}</span>
</div>
</div>
@empty
<p class="text-xs text-slate-400 italic text-center py-4">Görev yok</p>
@endforelse
</div>
<!-- Column 3: Review -->
<div class="bg-amber-50/60 dark:bg-amber-950/20 rounded-2xl p-4 space-y-3 border border-amber-500/20">
<div class="flex items-center justify-between pb-2 border-b border-amber-200 dark:border-amber-800/40">
<span class="text-xs font-bold uppercase tracking-wider text-amber-600 dark:text-amber-400 flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-amber-500"></span> Kontrol / Test
</span>
<span class="text-xs font-extrabold px-2 py-0.5 rounded-full bg-amber-200 dark:bg-amber-900/60 text-amber-700 dark:text-amber-300">
{{ $project->tasks->where('status', 'review')->count() }}
</span>
</div>
@forelse($project->tasks->where('status', 'review') as $task)
<div class="p-3.5 bg-white dark:bg-slate-900 rounded-xl border border-amber-200 dark:border-amber-900/40 shadow-sm space-y-2">
<h4 class="text-xs font-bold text-slate-900 dark:text-white">{{ $task->title }}</h4>
@if($task->description)
<p class="text-[11px] text-slate-500 line-clamp-2">{{ $task->description }}</p>
@endif
<div class="flex items-center justify-between text-[10px] font-semibold text-amber-600 dark:text-amber-400 pt-1">
<span>{{ $task->assigned_person ?? 'Test Ekibi' }}</span>
<span class="px-2 py-0.5 rounded bg-amber-100 dark:bg-amber-950 uppercase">{{ $task->priority }}</span>
</div>
</div>
@empty
<p class="text-xs text-slate-400 italic text-center py-4">Görev yok</p>
@endforelse
</div>
<!-- Column 4: Done -->
<div class="bg-emerald-50/60 dark:bg-emerald-950/20 rounded-2xl p-4 space-y-3 border border-emerald-500/20">
<div class="flex items-center justify-between pb-2 border-b border-emerald-200 dark:border-emerald-800/40">
<span class="text-xs font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span> Tamamlananlar
</span>
<span class="text-xs font-extrabold px-2 py-0.5 rounded-full bg-emerald-200 dark:bg-emerald-900/60 text-emerald-700 dark:text-emerald-300">
{{ $project->tasks->where('status', 'done')->count() }}
</span>
</div>
@forelse($project->tasks->where('status', 'done') as $task)
<div class="p-3.5 bg-white dark:bg-slate-900 rounded-xl border border-emerald-200 dark:border-emerald-900/40 shadow-sm space-y-2 opacity-90">
<h4 class="text-xs font-bold text-slate-900 dark:text-white line-through decoration-emerald-500">{{ $task->title }}</h4>
@if($task->description)
<p class="text-[11px] text-slate-500 line-clamp-2">{{ $task->description }}</p>
@endif
<div class="flex items-center justify-between text-[10px] font-semibold text-emerald-600 dark:text-emerald-400 pt-1">
<span>Tamamlandı</span>
<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-emerald-500"></i>
</div>
</div>
@empty
<p class="text-xs text-slate-400 italic text-center py-4">Görev yok</p>
@endforelse
</div>
</div>
</section>
<!-- Section 4: Live Activity Log Stream -->
<section id="updates-section" class="glass-card rounded-3xl p-6 sm:p-8 space-y-6">
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-xl bg-emerald-600 text-white flex items-center justify-center font-bold">
<i data-lucide="message-square" class="w-4 h-4"></i>
</div>
<div>
<h2 class="text-lg font-bold text-slate-900 dark:text-white font-display">4. Canlı İlerleme Duyuruları & Log Akışı</h2>
<p class="text-xs text-slate-500">Yazılım yöneticisinden gelen canlı proje güncellemeleri</p>
</div>
</div>
</div>
<!-- Timeline Stream -->
<div class="relative pl-6 border-l-2 border-orange-500/30 space-y-8 my-4">
@forelse($project->updates as $update)
<div class="relative group">
<!-- Bullet Dot -->
<div class="absolute -left-[31px] top-0 w-4 h-4 rounded-full bg-orange-600 border-4 border-slate-50 dark:border-slate-900 shadow-md"></div>
<div class="p-5 rounded-2xl bg-slate-100/60 dark:bg-slate-800/40 border border-slate-200 dark:border-slate-800 space-y-2">
<div class="flex items-center justify-between gap-4">
<h3 class="text-sm font-bold text-slate-900 dark:text-white">
{{ $update->title }}
</h3>
<span class="text-[11px] font-semibold text-slate-400">
{{ $update->created_at->format('d.m.Y H:i') }}
</span>
</div>
<p class="text-xs text-slate-600 dark:text-slate-300 leading-relaxed whitespace-pre-line">
{{ $update->content }}
</p>
@if($update->progress_percent_at_update !== null)
<div class="pt-2 flex items-center gap-2 text-xs font-semibold text-orange-600 dark:text-orange-400">
<i data-lucide="trending-up" class="w-3.5 h-3.5"></i>
<span>Güncelleme Anındaki İlerleme: %{{ $update->progress_percent_at_update }}</span>
</div>
@endif
</div>
</div>
@empty
<div class="text-center py-6 text-slate-400 text-sm italic">
Henüz yayınlanmış bir duyuru bulunmamaktadır.
</div>
@endforelse
</div>
</section>
</main>
<!-- Corporate Footer -->
<footer class="mt-16 border-t border-slate-200 dark:border-slate-800 py-8 text-center text-xs text-slate-500">
<div class="max-w-7xl mx-auto px-4 space-y-2">
<p>© {{ date('Y') }} <strong>Trunçgil Teknoloji Sanayi ve Ticaret Ltd. Şti.</strong> — Gaziantep Teknopark</p>
<p class="text-[11px] text-slate-400">Tüm geliştirmeler 4691 Sayılı Teknoloji Geliştirme Bölgeleri Kanunu Ar-Ge standartlarında yürütülmektedir.</p>
</div>
</footer>
<!-- JS Libraries -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.8.0/dist/mermaid.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@0.321.0/dist/umd/lucide.min.js"></script>
<script>
// Init Lucide Icons
lucide.createIcons();
// Dark Mode Controller
var themeToggleBtn = document.getElementById('theme-toggle');
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
themeToggleLightIcon.classList.remove('hidden');
} else {
document.documentElement.classList.remove('dark');
themeToggleDarkIcon.classList.remove('hidden');
}
themeToggleBtn.addEventListener('click', function() {
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
});
// Mermaid Init
const isDark = document.documentElement.classList.contains('dark');
mermaid.initialize({
startOnLoad: true,
theme: 'base',
securityLevel: 'loose',
themeVariables: {
fontFamily: 'Inter, sans-serif',
darkMode: isDark,
background: isDark ? '#0f172a' : '#ffffff',
primaryColor: '#ea580c',
primaryTextColor: '#ffffff',
primaryBorderColor: '#c2410c',
lineColor: '#ea580c',
secondaryColor: '#ef4444',
secondaryTextColor: '#ffffff',
tertiaryColor: isDark ? '#1e293b' : '#fff7ed',
sectionBkgColor: isDark ? 'rgba(234, 88, 12, 0.15)' : 'rgba(234, 88, 12, 0.07)',
sectionBkgColor2: isDark ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.07)',
gridColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.08)',
todayLineColor: '#ef4444'
}
});
let ganttPanZoom = null;
setTimeout(() => {
const svg = document.querySelector('#gantt-canvas svg');
if (svg) {
svg.style.width = '100%';
svg.style.height = '100%';
if (!svg.id) svg.id = 'gantt-svg';
try {
ganttPanZoom = svgPanZoom('#' + svg.id, {
zoomEnabled: true,
panEnabled: true,
fit: true,
center: true,
minZoom: 0.3,
maxZoom: 8,
onZoom: function(z) {
document.getElementById('gantt-zoom-pct').textContent = Math.round(z * 100) + '%';
}
});
} catch(e) {}
}
}, 300);
function zoomGantt(type) {
if (!ganttPanZoom) return;
if (type === 'in') ganttPanZoom.zoomIn();
else ganttPanZoom.zoomOut();
}
</script>
</body>
</html>
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=device-width, initial-scale=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=device-width, initial-scale=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="robots" content="noindex, nofollow">
<title>{{ $proposal->title }} - Trunçgil Teknoloji</title> <title>{{ $proposal->title }} - Trunçgil Teknoloji</title>
<!-- Google Fonts: Inter (Body), Outfit (Headings), Caveat (Signature Script) --> <!-- Google Fonts: Inter (Body), Outfit (Headings), Caveat (Signature Script) -->
+11
View File
@@ -174,6 +174,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::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'); Route::post('/teklif/{slug}/action', [\App\Http\Controllers\ProposalController::class, 'action'])->name('proposals.action');
// Project Tracking Client Portal & Web Admin Management
Route::get('/proje-takip/{slug}', [\App\Http\Controllers\ProjectController::class, 'show'])->name('projects.show');
Route::post('/proje-takip/{slug}/verify', [\App\Http\Controllers\ProjectController::class, 'verify'])->name('projects.verify');
Route::post('/proje-takip/{slug}/admin/module-status', [\App\Http\Controllers\ProjectController::class, 'updateModuleStatus'])->name('projects.admin.module-status');
Route::post('/proje-takip/{slug}/admin/task-status', [\App\Http\Controllers\ProjectController::class, 'updateTaskStatus'])->name('projects.admin.task-status');
Route::post('/proje-takip/{slug}/admin/add-task', [\App\Http\Controllers\ProjectController::class, 'addTask'])->name('projects.admin.add-task');
Route::post('/proje-takip/{slug}/admin/delete-task', [\App\Http\Controllers\ProjectController::class, 'deleteTask'])->name('projects.admin.delete-task');
Route::post('/proje-takip/{slug}/admin/add-update', [\App\Http\Controllers\ProjectController::class, 'addUpdate'])->name('projects.admin.add-update');
Route::post('/proje-takip/{slug}/admin/recalculate', [\App\Http\Controllers\ProjectController::class, 'recalculate'])->name('projects.admin.recalculate');
Route::post('/proje-takip/{slug}/admin/toggle-mode', [\App\Http\Controllers\ProjectController::class, 'toggleAdminMode'])->name('projects.admin.toggle-mode');
// Privacy Policy Alternatives // Privacy Policy Alternatives
Route::get('/privacy-policy', function () { Route::get('/privacy-policy', function () {
return app(PageController::class)->show('privacy'); return app(PageController::class)->show('privacy');