feat: implement full project management suite with module/task CRUD, progress tracking, and admin view toggling

This commit is contained in:
Ümit Tunç
2026-07-30 17:28:30 +03:00
parent 26cab615f1
commit 483b0518fd
2 changed files with 640 additions and 124 deletions
+178 -6
View File
@@ -3,29 +3,32 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectModule;
use App\Models\ProjectTask;
use App\Models\ProjectUpdate;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ProjectController extends Controller class ProjectController extends Controller
{ {
/** /**
* Display the public/client project tracking portal. * Display the public/client project tracking portal & manager view.
*/ */
public function show(Request $request, string $slug) public function show(Request $request, string $slug)
{ {
$project = Project::where('slug', $slug) $project = Project::where('slug', $slug)
->with(['modules', 'tasks', 'updates' => function($q) { ->with(['modules', 'tasks', 'updates' => function($q) {
$q->where('is_public', true)->latest(); $q->latest();
}, 'proposal']) }, 'proposal'])
->firstOrFail(); ->firstOrFail();
// Recalculate progress dynamically // Recalculate progress dynamically
$project->recalculateProgress(); $project->recalculateProgress();
// Check if access code protection is active and verified in session // Check if admin mode is active (logged in user OR session toggle)
$sessionKey = 'project_access_' . $project->id; $isAdminMode = Auth::check() || session()->get('admin_mode_' . $project->id, true);
$isVerified = session()->get($sessionKey, true); // Verified by default for link convenience
return view('projects.show', compact('project', 'isVerified')); return view('projects.show', compact('project', 'isAdminMode'));
} }
/** /**
@@ -46,4 +49,173 @@ class ProjectController extends Controller
return back()->withErrors(['access_code' => 'Geçersiz müşteri takip şifresi.']); return back()->withErrors(['access_code' => 'Geçersiz müşteri takip şifresi.']);
} }
/**
* Web Manager Action: Update Module Status (Bekliyor / Devam Ediyor / Tamamlandı)
*/
public function updateModuleStatus(Request $request, string $slug)
{
$project = Project::where('slug', $slug)->firstOrFail();
$request->validate([
'module_id' => 'required|exists:project_modules,id',
'status' => 'required|in:pending,in_progress,completed',
]);
$module = ProjectModule::where('id', $request->input('module_id'))
->where('project_id', $project->id)
->firstOrFail();
$module->update(['status' => $request->input('status')]);
// Recalculate overall progress %
$newProgress = $project->recalculateProgress();
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'message' => "'{$module->title}' modül durumu güncellendi.",
'progress_percent' => $newProgress,
]);
}
return back()->with('success', "'{$module->title}' modül durumu güncellendi.");
}
/**
* Web Manager Action: Update Task Status (Kanban Move)
*/
public function updateTaskStatus(Request $request, string $slug)
{
$project = Project::where('slug', $slug)->firstOrFail();
$request->validate([
'task_id' => 'required|exists:project_tasks,id',
'status' => 'required|in:todo,in_progress,review,done',
]);
$task = ProjectTask::where('id', $request->input('task_id'))
->where('project_id', $project->id)
->firstOrFail();
$task->update(['status' => $request->input('status')]);
$newProgress = $project->recalculateProgress();
if ($request->wantsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'message' => "'{$task->title}' görev durumu güncellendi.",
'progress_percent' => $newProgress,
'counts' => [
'todo' => $project->tasks()->where('status', 'todo')->count(),
'in_progress' => $project->tasks()->where('status', 'in_progress')->count(),
'review' => $project->tasks()->where('status', 'review')->count(),
'done' => $project->tasks()->where('status', 'done')->count(),
]
]);
}
return back()->with('success', "'{$task->title}' görev durumu güncellendi.");
}
/**
* Web Manager Action: Add New Task
*/
public function addTask(Request $request, string $slug)
{
$project = Project::where('slug', $slug)->firstOrFail();
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string|max:2000',
'status' => 'required|in:todo,in_progress,review,done',
'priority' => 'required|in:low,medium,high,urgent',
'assigned_person' => 'nullable|string|max:255',
'project_module_id' => 'nullable|exists:project_modules,id',
]);
$task = ProjectTask::create([
'project_id' => $project->id,
'project_module_id' => $request->input('project_module_id'),
'title' => $request->input('title'),
'description' => $request->input('description'),
'status' => $request->input('status'),
'priority' => $request->input('priority'),
'assigned_person' => $request->input('assigned_person'),
]);
$project->recalculateProgress();
return back()->with('success', "'{$task->title}' görevi panoya eklendi.");
}
/**
* Web Manager Action: Delete Task
*/
public function deleteTask(Request $request, string $slug)
{
$project = Project::where('slug', $slug)->firstOrFail();
$request->validate([
'task_id' => 'required|exists:project_tasks,id',
]);
$task = ProjectTask::where('id', $request->input('task_id'))
->where('project_id', $project->id)
->firstOrFail();
$taskName = $task->title;
$task->delete();
$project->recalculateProgress();
return back()->with('success', "'{$taskName}' görevi silindi.");
}
/**
* Web Manager Action: Add Live Progress Update / Announcement
*/
public function addUpdate(Request $request, string $slug)
{
$project = Project::where('slug', $slug)->firstOrFail();
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string|max:5000',
]);
ProjectUpdate::create([
'project_id' => $project->id,
'user_id' => Auth::id(),
'title' => $request->input('title'),
'content' => $request->input('content'),
'progress_percent_at_update' => $project->progress_percent,
'is_public' => true,
]);
return back()->with('success', 'Yeni ilerleme duyurusu yayınlandı.');
}
/**
* Web Manager Action: Recalculate Progress %
*/
public function recalculate(Request $request, string $slug)
{
$project = Project::where('slug', $slug)->firstOrFail();
$pct = $project->recalculateProgress();
return back()->with('success', "Proje ilerleme yüzdesi yeniden hesaplandı: %{$pct}");
}
/**
* Toggle Admin / Client View Mode in session
*/
public function toggleAdminMode(Request $request, string $slug)
{
$project = Project::where('slug', $slug)->firstOrFail();
$key = 'admin_mode_' . $project->id;
$current = session()->get($key, true);
session()->put($key, !$current);
return back();
}
} }
+462 -118
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=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"> <meta name="robots" content="noindex, nofollow">
<title>{{ $project->title }} - Canlı Proje Takip Portalı | Trunçgil Teknoloji</title> <title>{{ $project->title }} - Canlı Proje Takip & Yönetim Portalı | Trunçgil Teknoloji</title>
<!-- Google Fonts: Inter & Outfit --> <!-- Google Fonts: Inter & Outfit -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -40,21 +40,21 @@
<style type="text/css"> <style type="text/css">
.glass-header { .glass-header {
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(0, 0, 0, 0.06); border-bottom: 1px solid rgba(0, 0, 0, 0.06);
} }
.dark .glass-header { .dark .glass-header {
background: rgba(15, 23, 42, 0.8); background: rgba(15, 23, 42, 0.85);
border-bottom: 1px solid rgba(255, 255, 255, 0.06); border-bottom: 1px solid rgba(255, 255, 255, 0.06);
} }
.glass-card { .glass-card {
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
} }
.dark .glass-card { .dark .glass-card {
background: rgba(30, 41, 59, 0.7); background: rgba(30, 41, 59, 0.75);
border: 1px solid rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.06);
} }
.mermaid-container { .mermaid-container {
@@ -122,12 +122,67 @@
.dark .mermaid svg .section1, .dark .mermaid svg .section3 { .dark .mermaid svg .section1, .dark .mermaid svg .section3 {
fill: rgba(239, 68, 68, 0.18) !important; fill: rgba(239, 68, 68, 0.18) !important;
} }
/* Sortable Dragging Styling */
.sortable-ghost {
opacity: 0.3 !important;
background: rgba(234, 88, 12, 0.15) !important;
border: 2px dashed #ea580c !important;
}
.sortable-drag {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2), 0 10px 10px -5px rgba(0, 0, 0, 0.1) !important;
transform: rotate(2deg) scale(1.03) !important;
}
</style> </style>
</head> </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"> <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">
<!-- Floating Toast Notification -->
<div id="toast-notification" class="fixed bottom-6 right-6 z-50 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-none">
<div class="bg-slate-900 text-white px-5 py-3 rounded-2xl shadow-2xl border border-slate-700 flex items-center gap-3">
<div id="toast-icon" class="w-6 h-6 rounded-full bg-emerald-500 text-white flex items-center justify-center flex-shrink-0 font-bold text-xs">✓</div>
<span id="toast-message" class="text-xs font-bold">İşlem başarıyla gerçekleşti.</span>
</div>
</div>
<!-- Admin / Manager Floating Top Control Bar -->
<div class="bg-gradient-to-r from-amber-600 via-orange-600 to-red-600 text-white px-4 py-2.5 shadow-lg border-b border-orange-500/40 sticky top-0 z-50">
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">
<div class="flex items-center gap-2 font-bold uppercase tracking-wider">
<span class="w-2.5 h-2.5 rounded-full bg-white animate-ping"></span>
<i data-lucide="shield-check" class="w-4 h-4"></i>
<span>WEB YÖNETİCİ DÜZENLEME MODU (INTERACTIVE AJAX & DRAG-AND-DROP)</span>
</div>
<div class="flex flex-wrap items-center gap-2">
<button onclick="openAddTaskModal()" class="px-3 py-1.5 rounded-lg bg-white/20 hover:bg-white/30 font-bold transition-all flex items-center gap-1">
<i data-lucide="plus-circle" class="w-3.5 h-3.5"></i>
<span>+ Yeni Görev Ekle</span>
</button>
<button onclick="openAddUpdateModal()" class="px-3 py-1.5 rounded-lg bg-white/20 hover:bg-white/30 font-bold transition-all flex items-center gap-1">
<i data-lucide="message-square-plus" class="w-3.5 h-3.5"></i>
<span>+ Duyuru Yayınla</span>
</button>
<form action="{{ route('projects.admin.recalculate', $project->slug) }}" method="POST" class="inline">
@csrf
<button type="submit" class="px-3 py-1.5 rounded-lg bg-white/20 hover:bg-white/30 font-bold transition-all flex items-center gap-1">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
<span>Yeniden Hesapla</span>
</button>
</form>
<a href="/admin/projects/{{ $project->id }}/edit" target="_blank" class="px-3 py-1.5 rounded-lg bg-white text-orange-700 hover:bg-orange-50 font-bold transition-all flex items-center gap-1 shadow-sm">
<i data-lucide="layout-dashboard" class="w-3.5 h-3.5"></i>
<span>Filament Paneli</span>
</a>
</div>
</div>
</div>
<!-- Header Navigation --> <!-- Header Navigation -->
<header class="sticky top-0 z-40 w-full glass-header"> <header class="w-full glass-header sticky top-[41px] z-40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-18 flex items-center justify-between py-4"> <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 --> <!-- Logos -->
@@ -138,7 +193,7 @@
</a> </a>
<div class="h-6 w-px bg-slate-300 dark:bg-slate-700"></div> <div class="h-6 w-px bg-slate-300 dark:bg-slate-700"></div>
<div class="flex flex-col"> <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-xs font-bold uppercase tracking-wider text-orange-600 dark:text-orange-400">Canlı Proje Takip & Yönetim Portalı</span>
<span class="text-sm font-semibold text-slate-700 dark:text-slate-200 line-clamp-1">{{ $project->client_name }}</span> <span class="text-sm font-semibold text-slate-700 dark:text-slate-200 line-clamp-1">{{ $project->client_name }}</span>
</div> </div>
</div> </div>
@@ -164,6 +219,19 @@
<!-- Main Container --> <!-- Main Container -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8"> <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
<!-- Flash Session Notifications -->
@if(session('success'))
<div class="bg-emerald-50 dark:bg-emerald-950/50 border border-emerald-300 dark:border-emerald-800 text-emerald-800 dark:text-emerald-200 p-4 rounded-2xl flex items-center justify-between shadow-md">
<div class="flex items-center gap-2.5 font-bold text-sm">
<i data-lucide="check-circle-2" class="w-5 h-5 text-emerald-600"></i>
<span>{{ session('success') }}</span>
</div>
<button onclick="this.parentElement.remove()" class="text-emerald-500 hover:text-emerald-700">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
@endif
<!-- Hero Section & Progress Banner --> <!-- 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="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="absolute -top-24 -right-24 w-96 h-96 bg-orange-600/20 rounded-full blur-3xl pointer-events-none"></div>
@@ -173,7 +241,7 @@
<div class="lg:col-span-8 space-y-4"> <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"> <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 class="w-2 h-2 rounded-full bg-orange-400 animate-ping"></span>
<span>MÜŞTERİ CANLI TAKİP PANELİ</span> <span>CANLI PROJE YÖNETİM & TAKİP EKRANI</span>
</div> </div>
<h1 class="text-2xl sm:text-4xl font-extrabold font-display leading-tight text-white"> <h1 class="text-2xl sm:text-4xl font-extrabold font-display leading-tight text-white">
@@ -181,7 +249,7 @@
</h1> </h1>
<p class="text-sm sm:text-base text-slate-300 font-medium"> <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. Bu ekran, <strong>{{ $project->client_name }}</strong> projesine ait iş takvimini, modül durumlarını ve sürükle-bırak destekli canlı Kanban panosunu yönetebileceğiniz portal ekranıdır.
</p> </p>
<!-- Meta Tags --> <!-- Meta Tags -->
@@ -211,20 +279,19 @@
<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"> <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> <span class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">GENEL PROJE İLERLEMESİ</span>
<!-- Progress Donut/Bar --> <div id="progress-percent-val" class="text-5xl font-extrabold font-display text-transparent bg-clip-text bg-gradient-to-r from-orange-400 to-rose-400 mb-3">
<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 }} %{{ $project->progress_percent }}
</div> </div>
<div class="w-full bg-slate-700 h-3 rounded-full overflow-hidden mb-3"> <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 id="progress-bar-fill" class="bg-gradient-to-r from-orange-500 to-rose-500 h-full rounded-full transition-all duration-700 shadow-lg shadow-orange-500/50" style="width: {{ $project->progress_percent }}%"></div>
</div> </div>
<span class="text-xs text-slate-300 font-medium text-center"> <span id="progress-status-text" class="text-xs text-slate-300 font-medium text-center">
@if($project->progress_percent >= 100) @if($project->progress_percent >= 100)
🎉 Proje %100 Başarıyla Tamamlandı! 🎉 Proje %100 Başarıyla Tamamlandı!
@elseif($project->progress_percent >= 50) @elseif($project->progress_percent >= 50)
⚡ Proje Geliştirmeleri Hızla Devam Ediyor ⚡ Geliştirmeler Hızla Devam Ediyor
@else @else
🚀 Faz 1 Çalışmaları Başlatıldı 🚀 Faz 1 Çalışmaları Başlatıldı
@endif @endif
@@ -233,7 +300,7 @@
</div> </div>
</div> </div>
<!-- Interactive Section Tabs --> <!-- Section Navigation Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4"> <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"> <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"> <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">
@@ -251,7 +318,7 @@
</div> </div>
<div> <div>
<h4 class="text-sm font-bold text-slate-900 dark:text-white">Hizmet Modülleri</h4> <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> <p class="text-xs text-slate-500"><span id="completed-modules-count">{{ $project->modules->where('status', 'completed')->count() }}</span> / {{ $project->modules->count() }} Modül Bitti</p>
</div> </div>
</a> </a>
@@ -261,7 +328,7 @@
</div> </div>
<div> <div>
<h4 class="text-sm font-bold text-slate-900 dark:text-white">Kanban Panosu</h4> <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> <p class="text-xs text-slate-500"><span id="total-tasks-count">{{ $project->tasks->count() }}</span> Adet Sürükle-Bırak Kartı</p>
</div> </div>
</a> </a>
@@ -271,7 +338,7 @@
</div> </div>
<div> <div>
<h4 class="text-sm font-bold text-slate-900 dark:text-white">Canlı Log Akışı</h4> <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> <p class="text-xs text-slate-500">{{ $project->updates->count() }} Duyuru Yayınlandı</p>
</div> </div>
</a> </a>
</div> </div>
@@ -319,63 +386,77 @@ gantt
</div> </div>
</section> </section>
<!-- Section 2: Modules Completion Matrix --> <!-- Section 2: Modules Completion Matrix (INTERACTIVE AJAX STATUS TOGGLES) -->
<section id="modules-section" class="glass-card rounded-3xl p-6 sm:p-8 space-y-6"> <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 flex-col sm:flex-row sm:items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-4 gap-4">
<div class="flex items-center gap-3"> <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"> <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> <i data-lucide="check-square" class="w-4 h-4"></i>
</div> </div>
<div> <div>
<h2 class="text-lg font-bold text-slate-900 dark:text-white font-display">2. Hizmet Modülleri İlerleme Matrisi</h2> <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> <p class="text-xs text-slate-500">Sayfa yenilemeden tek tıkla modül durumlarını canlı değiştirin</p>
</div> </div>
</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"> <span class="text-xs font-bold px-3 py-1.5 rounded-full bg-orange-500/10 text-orange-600 dark:text-orange-400 border border-orange-500/20 self-start sm:self-auto">
Toplam {{ $project->modules->count() }} Modül ⚡ Canlı AJAX Düzenleme
</span> </span>
</div> </div>
<!-- Modules Grid --> <!-- Modules Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@forelse($project->modules as $module) @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 id="module-card-{{ $module->id }}" 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-start justify-between gap-3 mb-3">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
@if($module->status === 'completed') <div id="module-icon-{{ $module->id }}">
<div class="w-6 h-6 rounded-full bg-emerald-500 text-white flex items-center justify-center flex-shrink-0"> @if($module->status === 'completed')
<i data-lucide="check" class="w-3.5 h-3.5"></i> <div class="w-6 h-6 rounded-full bg-emerald-500 text-white flex items-center justify-center flex-shrink-0">
</div> <i data-lucide="check" class="w-3.5 h-3.5"></i>
@elseif($module->status === 'in_progress') </div>
<div class="w-6 h-6 rounded-full bg-orange-500 text-white flex items-center justify-center flex-shrink-0 animate-pulse"> @elseif($module->status === 'in_progress')
<i data-lucide="play" class="w-3 h-3"></i> <div class="w-6 h-6 rounded-full bg-orange-500 text-white flex items-center justify-center flex-shrink-0 animate-pulse">
</div> <i data-lucide="play" class="w-3 h-3"></i>
@else </div>
<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"> @else
<i data-lucide="clock" class="w-3.5 h-3.5"></i> <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">
</div> <i data-lucide="clock" class="w-3.5 h-3.5"></i>
@endif </div>
@endif
</div>
<h3 class="font-bold text-slate-900 dark:text-white text-base"> <h3 class="font-bold text-slate-900 dark:text-white text-base">
{{ $module->title }} {{ $module->title }}
</h3> </h3>
</div> </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') }}"> <span id="module-badge-{{ $module->id }}" 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 @if($module->status === 'completed') TAMAMLANDI @elseif($module->status === 'in_progress') DEVAM EDİYOR @else BEKLİYOR @endif
</span> </span>
</div> </div>
@if($module->description) @if($module->description)
<p class="text-xs text-slate-600 dark:text-slate-400 leading-relaxed mb-3 pl-8"> <p class="text-xs text-slate-600 dark:text-slate-400 leading-relaxed mb-4 pl-8">
{{ $module->description }} {{ $module->description }}
</p> </p>
@endif @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"> <!-- Admin Web Status Toggle Buttons (AJAX enabled) -->
<span class="text-slate-500">Ağırlık: %{{ $module->weight_percent }}</span> <div class="pt-3 border-t border-slate-200/60 dark:border-slate-800/60 flex flex-wrap items-center justify-between gap-2">
@if($module->start_date && $module->end_date) <span class="text-xs font-semibold text-slate-500">Ağırlık: %{{ $module->weight_percent }}</span>
<span class="text-slate-400">{{ $module->start_date->format('d.m') }} - {{ $module->end_date->format('d.m.Y') }}</span>
@endif <div class="flex items-center gap-1.5">
<button onclick="setModuleStatusAjax({{ $module->id }}, 'pending')" class="px-2.5 py-1 rounded-lg text-xs font-bold transition-all hover:scale-105 active:scale-95 {{ $module->status === 'pending' ? 'bg-slate-700 text-white shadow-sm' : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-300' }}">
Bekliyor
</button>
<button onclick="setModuleStatusAjax({{ $module->id }}, 'in_progress')" class="px-2.5 py-1 rounded-lg text-xs font-bold transition-all hover:scale-105 active:scale-95 {{ $module->status === 'in_progress' ? 'bg-orange-600 text-white shadow-sm' : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-orange-100 dark:hover:bg-orange-950' }}">
Devam Ediyor
</button>
<button onclick="setModuleStatusAjax({{ $module->id }}, 'completed')" class="px-2.5 py-1 rounded-lg text-xs font-bold transition-all hover:scale-105 active:scale-95 {{ $module->status === 'completed' ? 'bg-emerald-600 text-white shadow-sm' : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-emerald-100 dark:hover:bg-emerald-950' }}">
✓ Tamamlandı
</button>
</div>
</div> </div>
</div> </div>
@empty @empty
@@ -386,129 +467,162 @@ gantt
</div> </div>
</section> </section>
<!-- Section 3: Live Kanban Board --> <!-- Section 3: Live Drag-and-Drop Kanban Board (Sortable.js + AJAX) -->
<section id="kanban-section" class="glass-card rounded-3xl p-6 sm:p-8 space-y-6"> <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 flex-col sm:flex-row sm:items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-4 gap-4">
<div class="flex items-center gap-3"> <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"> <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> <i data-lucide="kanban-square" class="w-4 h-4"></i>
</div> </div>
<div> <div>
<h2 class="text-lg font-bold text-slate-900 dark:text-white font-display">3. Canlı Kanban Görev Panosu</h2> <h2 class="text-lg font-bold text-slate-900 dark:text-white font-display">3. Canlı Sürükle-Bırak 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> <p class="text-xs text-slate-500">Kartları kolonlar arasında sürükleyip bırakarak sayfa yenilemeden canlı güncelleyin</p>
</div> </div>
</div> </div>
<button onclick="openAddTaskModal()" class="px-4 py-2 rounded-xl bg-orange-600 hover:bg-orange-700 text-white font-bold text-xs shadow-md transition-all flex items-center gap-1.5 self-start sm:self-auto">
<i data-lucide="plus" class="w-4 h-4"></i>
<span>Yeni Görev Ekle</span>
</button>
</div> </div>
<!-- 4 Column Kanban Board --> <!-- 4 Column Drag and Drop Board -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Column 1: To Do --> <!-- Column 1: To Do -->
<div class="bg-slate-100/70 dark:bg-slate-800/50 rounded-2xl p-4 space-y-3"> <div class="bg-slate-100/70 dark:bg-slate-800/50 rounded-2xl p-4 space-y-3 border border-slate-200 dark:border-slate-700 flex flex-col">
<div class="flex items-center justify-between pb-2 border-b border-slate-200 dark:border-slate-700"> <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="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 class="w-2 h-2 rounded-full bg-slate-400"></span> YAPILACAKLAR
</span> </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"> <span id="count-todo" 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() }} {{ $project->tasks->where('status', 'todo')->count() }}
</span> </span>
</div> </div>
@forelse($project->tasks->where('status', 'todo') as $task) <div class="kanban-drop-zone min-h-[220px] flex-1 space-y-3 pt-1" data-status="todo">
<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"> @foreach($project->tasks->where('status', 'todo') as $task)
<h4 class="text-xs font-bold text-slate-900 dark:text-white">{{ $task->title }}</h4> <div class="kanban-card cursor-grab active:cursor-grabbing p-3.5 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm space-y-2.5 transition-all hover:shadow-md" data-task-id="{{ $task->id }}">
@if($task->description) <div class="flex items-start justify-between gap-2">
<p class="text-[11px] text-slate-500 line-clamp-2">{{ $task->description }}</p> <h4 class="text-xs font-bold text-slate-900 dark:text-white leading-snug">{{ $task->title }}</h4>
@endif <button onclick="deleteTaskAjax({{ $task->id }}, this)" class="text-slate-400 hover:text-red-500 p-0.5 flex-shrink-0">
<div class="flex items-center justify-between text-[10px] font-semibold text-slate-400 pt-1"> <i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>{{ $task->assigned_person ?? 'Atanmadı' }}</span> </button>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 uppercase">{{ $task->priority }}</span> </div>
@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.5 border-t border-slate-100 dark:border-slate-800">
<span>{{ $task->assigned_person ?? 'Atanmadı' }}</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 uppercase text-slate-600 dark:text-slate-300 font-bold">{{ $task->priority }}</span>
</div>
</div> </div>
</div> @endforeach
@empty </div>
<p class="text-xs text-slate-400 italic text-center py-4">Görev yok</p>
@endforelse
</div> </div>
<!-- Column 2: In Progress --> <!-- 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="bg-orange-50/60 dark:bg-orange-950/20 rounded-2xl p-4 space-y-3 border border-orange-500/20 flex flex-col">
<div class="flex items-center justify-between pb-2 border-b border-orange-200 dark:border-orange-800/40"> <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="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 class="w-2 h-2 rounded-full bg-orange-500 animate-ping"></span> DEVAM EDENLER
</span> </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"> <span id="count-in_progress" 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() }} {{ $project->tasks->where('status', 'in_progress')->count() }}
</span> </span>
</div> </div>
@forelse($project->tasks->where('status', 'in_progress') as $task) <div class="kanban-drop-zone min-h-[220px] flex-1 space-y-3 pt-1" data-status="in_progress">
<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"> @foreach($project->tasks->where('status', 'in_progress') as $task)
<h4 class="text-xs font-bold text-slate-900 dark:text-white">{{ $task->title }}</h4> <div class="kanban-card cursor-grab active:cursor-grabbing 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.5 transition-all hover:shadow-md" data-task-id="{{ $task->id }}">
@if($task->description) <div class="flex items-start justify-between gap-2">
<p class="text-[11px] text-slate-500 line-clamp-2">{{ $task->description }}</p> <h4 class="text-xs font-bold text-slate-900 dark:text-white leading-snug">{{ $task->title }}</h4>
@endif <button onclick="deleteTaskAjax({{ $task->id }}, this)" class="text-slate-400 hover:text-red-500 p-0.5 flex-shrink-0">
<div class="flex items-center justify-between text-[10px] font-semibold text-orange-600 dark:text-orange-400 pt-1"> <i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>{{ $task->assigned_person ?? 'Yazılım Ekibi' }}</span> </button>
<span class="px-2 py-0.5 rounded bg-orange-100 dark:bg-orange-950 uppercase">{{ $task->priority }}</span> </div>
@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.5 border-t border-slate-100 dark:border-slate-800">
<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 font-bold">{{ $task->priority }}</span>
</div>
</div> </div>
</div> @endforeach
@empty </div>
<p class="text-xs text-slate-400 italic text-center py-4">Görev yok</p>
@endforelse
</div> </div>
<!-- Column 3: Review --> <!-- 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="bg-amber-50/60 dark:bg-amber-950/20 rounded-2xl p-4 space-y-3 border border-amber-500/20 flex flex-col">
<div class="flex items-center justify-between pb-2 border-b border-amber-200 dark:border-amber-800/40"> <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="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 class="w-2 h-2 rounded-full bg-amber-500"></span> KONTROL / TEST
</span> </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"> <span id="count-review" 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() }} {{ $project->tasks->where('status', 'review')->count() }}
</span> </span>
</div> </div>
@forelse($project->tasks->where('status', 'review') as $task) <div class="kanban-drop-zone min-h-[220px] flex-1 space-y-3 pt-1" data-status="review">
<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"> @foreach($project->tasks->where('status', 'review') as $task)
<h4 class="text-xs font-bold text-slate-900 dark:text-white">{{ $task->title }}</h4> <div class="kanban-card cursor-grab active:cursor-grabbing 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.5 transition-all hover:shadow-md" data-task-id="{{ $task->id }}">
@if($task->description) <div class="flex items-start justify-between gap-2">
<p class="text-[11px] text-slate-500 line-clamp-2">{{ $task->description }}</p> <h4 class="text-xs font-bold text-slate-900 dark:text-white leading-snug">{{ $task->title }}</h4>
@endif <button onclick="deleteTaskAjax({{ $task->id }}, this)" class="text-slate-400 hover:text-red-500 p-0.5 flex-shrink-0">
<div class="flex items-center justify-between text-[10px] font-semibold text-amber-600 dark:text-amber-400 pt-1"> <i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>{{ $task->assigned_person ?? 'Test Ekibi' }}</span> </button>
<span class="px-2 py-0.5 rounded bg-amber-100 dark:bg-amber-950 uppercase">{{ $task->priority }}</span> </div>
@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.5 border-t border-slate-100 dark:border-slate-800">
<span>{{ $task->assigned_person ?? 'Test Ekibi' }}</span>
<span class="px-2 py-0.5 rounded bg-amber-100 dark:bg-amber-950 uppercase font-bold">{{ $task->priority }}</span>
</div>
</div> </div>
</div> @endforeach
@empty </div>
<p class="text-xs text-slate-400 italic text-center py-4">Görev yok</p>
@endforelse
</div> </div>
<!-- Column 4: Done --> <!-- 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="bg-emerald-50/60 dark:bg-emerald-950/20 rounded-2xl p-4 space-y-3 border border-emerald-500/20 flex flex-col">
<div class="flex items-center justify-between pb-2 border-b border-emerald-200 dark:border-emerald-800/40"> <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="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 class="w-2 h-2 rounded-full bg-emerald-500"></span> TAMAMLANANLAR
</span> </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"> <span id="count-done" 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() }} {{ $project->tasks->where('status', 'done')->count() }}
</span> </span>
</div> </div>
@forelse($project->tasks->where('status', 'done') as $task) <div class="kanban-drop-zone min-h-[220px] flex-1 space-y-3 pt-1" data-status="done">
<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"> @foreach($project->tasks->where('status', 'done') as $task)
<h4 class="text-xs font-bold text-slate-900 dark:text-white line-through decoration-emerald-500">{{ $task->title }}</h4> <div class="kanban-card cursor-grab active:cursor-grabbing 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.5 transition-all hover:shadow-md opacity-95" data-task-id="{{ $task->id }}">
@if($task->description) <div class="flex items-start justify-between gap-2">
<p class="text-[11px] text-slate-500 line-clamp-2">{{ $task->description }}</p> <h4 class="text-xs font-bold text-slate-900 dark:text-white leading-snug line-through decoration-emerald-500">{{ $task->title }}</h4>
@endif <button onclick="deleteTaskAjax({{ $task->id }}, this)" class="text-slate-400 hover:text-red-500 p-0.5 flex-shrink-0">
<div class="flex items-center justify-between text-[10px] font-semibold text-emerald-600 dark:text-emerald-400 pt-1"> <i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>Tamamlandı</span> </button>
<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-emerald-500"></i> </div>
@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.5 border-t border-slate-100 dark:border-slate-800">
<span>Tamamlandı</span>
<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-emerald-500"></i>
</div>
</div> </div>
</div> @endforeach
@empty </div>
<p class="text-xs text-slate-400 italic text-center py-4">Görev yok</p>
@endforelse
</div> </div>
</div> </div>
@@ -516,23 +630,45 @@ gantt
<!-- Section 4: Live Activity Log Stream --> <!-- Section 4: Live Activity Log Stream -->
<section id="updates-section" class="glass-card rounded-3xl p-6 sm:p-8 space-y-6"> <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 flex-col sm:flex-row sm:items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-4 gap-4">
<div class="flex items-center gap-3"> <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"> <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> <i data-lucide="message-square" class="w-4 h-4"></i>
</div> </div>
<div> <div>
<h2 class="text-lg font-bold text-slate-900 dark:text-white font-display">4. Canlı İlerleme Duyuruları & Log Akışı</h2> <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> <p class="text-xs text-slate-500">Müşterinin takip ettiği anlık durum mesajları ve sürüm açıklamaları</p>
</div> </div>
</div> </div>
<button onclick="openAddUpdateModal()" class="px-4 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs shadow-md transition-all flex items-center gap-1.5 self-start sm:self-auto">
<i data-lucide="plus" class="w-4 h-4"></i>
<span>Yeni Duyuru Yayınla</span>
</button>
</div>
<!-- Fast Add Update Box -->
<div class="bg-slate-100/70 dark:bg-slate-800/60 p-4 sm:p-5 rounded-2xl border border-slate-200 dark:border-slate-700">
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-300 mb-3 flex items-center gap-1.5">
<i data-lucide="send" class="w-3.5 h-3.5 text-emerald-500"></i>
<span>Hızlı Canlı Duyuru / Log Ekle</span>
</h3>
<form action="{{ route('projects.admin.add-update', $project->slug) }}" method="POST" class="space-y-3">
@csrf
<input type="text" name="title" required placeholder="Duyuru Başlığı (Örn: WhatsApp Webhook Testleri Başlatıldı)" class="w-full px-3.5 py-2 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-800 dark:text-white focus:outline-none focus:border-orange-500">
<textarea name="content" required rows="2" placeholder="Duyuru detayları ve açıklama notu..." class="w-full px-3.5 py-2 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs text-slate-800 dark:text-white focus:outline-none focus:border-orange-500"></textarea>
<div class="flex justify-end">
<button type="submit" class="px-4 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs transition-all shadow-sm">
Duyuruyu Yayınla →
</button>
</div>
</form>
</div> </div>
<!-- Timeline Stream --> <!-- Timeline Stream -->
<div class="relative pl-6 border-l-2 border-orange-500/30 space-y-8 my-4"> <div class="relative pl-6 border-l-2 border-orange-500/30 space-y-8 my-4">
@forelse($project->updates as $update) @forelse($project->updates as $update)
<div class="relative group"> <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="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="p-5 rounded-2xl bg-slate-100/60 dark:bg-slate-800/40 border border-slate-200 dark:border-slate-800 space-y-2">
@@ -567,6 +703,66 @@ gantt
</main> </main>
<!-- Add Task Modal -->
<div id="add-task-modal" class="fixed inset-0 z-50 bg-slate-900/80 backdrop-blur-sm hidden flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-900 rounded-3xl max-w-lg w-full p-6 space-y-4 shadow-2xl border border-slate-200 dark:border-slate-800">
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-3">
<h3 class="font-bold text-base text-slate-900 dark:text-white flex items-center gap-2">
<i data-lucide="plus-circle" class="w-4 h-4 text-orange-500"></i>
<span>Panoya Yeni Görev Kartı Ekle</span>
</h3>
<button onclick="closeAddTaskModal()" class="text-slate-400 hover:text-slate-600">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<form action="{{ route('projects.admin.add-task', $project->slug) }}" method="POST" class="space-y-4">
@csrf
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Görev Başlığı *</label>
<input type="text" name="title" required placeholder="Örn: Meta WhatsApp Webhook Testleri" class="w-full px-3.5 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white focus:outline-none focus:border-orange-500">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Açıklama / Detaylar</label>
<textarea name="description" rows="3" placeholder="Görev detayları..." class="w-full px-3.5 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 text-xs text-slate-900 dark:text-white focus:outline-none focus:border-orange-500"></textarea>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Başlangıç Durumu</label>
<select name="status" class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white">
<option value="todo">Yapılacaklar</option>
<option value="in_progress">Devam Edenler</option>
<option value="review">Kontrol / Test</option>
<option value="done">Tamamlananlar</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Öncelik</label>
<select name="priority" class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white">
<option value="low">Düşük</option>
<option value="medium" selected>Orta</option>
<option value="high">Yüksek</option>
<option value="urgent">Acil</option>
</select>
</div>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Sorumlu Kişi / Ekip</label>
<input type="text" name="assigned_person" placeholder="Örn: Backend Ekibi" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white focus:outline-none focus:border-orange-500">
</div>
<div class="pt-2 flex justify-end gap-2">
<button type="button" onclick="closeAddTaskModal()" class="px-4 py-2 rounded-xl bg-slate-200 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-bold text-xs">İptal</button>
<button type="submit" class="px-4 py-2 rounded-xl bg-orange-600 hover:bg-orange-700 text-white font-bold text-xs shadow-md">Görevi Ekle →</button>
</div>
</form>
</div>
</div>
<!-- Corporate Footer --> <!-- Corporate Footer -->
<footer class="mt-16 border-t border-slate-200 dark:border-slate-800 py-8 text-center text-xs text-slate-500"> <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"> <div class="max-w-7xl mx-auto px-4 space-y-2">
@@ -579,9 +775,10 @@ gantt
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.8.0/dist/mermaid.min.js"></script> <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/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 src="https://cdn.jsdelivr.net/npm/lucide@0.321.0/dist/umd/lucide.min.js"></script>
<!-- Sortable JS for Drag and Drop -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script> <script>
// Init Lucide Icons
lucide.createIcons(); lucide.createIcons();
// Dark Mode Controller // Dark Mode Controller
@@ -661,6 +858,153 @@ gantt
if (type === 'in') ganttPanZoom.zoomIn(); if (type === 'in') ganttPanZoom.zoomIn();
else ganttPanZoom.zoomOut(); else ganttPanZoom.zoomOut();
} }
// Sortable.js Drag and Drop Setup for 4 Kanban Columns
document.querySelectorAll('.kanban-drop-zone').forEach(zone => {
new Sortable(zone, {
group: 'kanban-board',
animation: 200,
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
onEnd: function(evt) {
const itemEl = evt.item;
const targetZone = evt.to;
const taskId = itemEl.getAttribute('data-task-id');
const newStatus = targetZone.getAttribute('data-status');
if (!taskId || !newStatus) return;
fetch('{{ route("projects.admin.task-status", $project->slug) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
task_id: taskId,
status: newStatus
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
updateProgressUI(data.progress_percent);
if (data.counts) {
updateCountsUI(data.counts);
}
showToast(data.message, 'success');
} else {
showToast('Görev taşınamadı.', 'error');
}
})
.catch(err => {
console.error('AJAX Error:', err);
showToast('Bağlantı hatası.', 'error');
});
}
});
});
// AJAX Module Status Toggle Handler
function setModuleStatusAjax(moduleId, status) {
fetch('{{ route("projects.admin.module-status", $project->slug) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
module_id: moduleId,
status: status
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
updateProgressUI(data.progress_percent);
showToast(data.message, 'success');
setTimeout(() => location.reload(), 400);
}
});
}
// AJAX Delete Task
function deleteTaskAjax(taskId, btnEl) {
if (!confirm('Bu görevi silmek istediğinize emin misiniz?')) return;
fetch('{{ route("projects.admin.delete-task", $project->slug) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
task_id: taskId
})
})
.then(res => {
const card = btnEl.closest('.kanban-card');
if (card) card.remove();
showToast('Görev silindi.', 'success');
setTimeout(() => location.reload(), 500);
});
}
// Dynamic UI Updates
function updateProgressUI(pct) {
const valEl = document.getElementById('progress-percent-val');
const barFill = document.getElementById('progress-bar-fill');
if (valEl) valEl.textContent = '%' + pct;
if (barFill) barFill.style.width = pct + '%';
}
function updateCountsUI(counts) {
if (counts.todo !== undefined) document.getElementById('count-todo').textContent = counts.todo;
if (counts.in_progress !== undefined) document.getElementById('count-in_progress').textContent = counts.in_progress;
if (counts.review !== undefined) document.getElementById('count-review').textContent = counts.review;
if (counts.done !== undefined) document.getElementById('count-done').textContent = counts.done;
}
// Floating Toast Notification
function showToast(msg, type = 'success') {
const toast = document.getElementById('toast-notification');
const toastMsg = document.getElementById('toast-message');
const toastIcon = document.getElementById('toast-icon');
if (toastMsg) toastMsg.textContent = msg;
if (toastIcon) {
toastIcon.className = 'w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 font-bold text-xs ' +
(type === 'success' ? 'bg-emerald-500 text-white' : 'bg-red-500 text-white');
toastIcon.textContent = type === 'success' ? '✓' : '!';
}
if (toast) {
toast.classList.remove('translate-y-20', 'opacity-0');
toast.classList.add('translate-y-0', 'opacity-100');
setTimeout(() => {
toast.classList.remove('translate-y-0', 'opacity-100');
toast.classList.add('translate-y-20', 'opacity-0');
}, 3000);
}
}
// Modal Handlers
function openAddTaskModal() {
document.getElementById('add-task-modal').classList.remove('hidden');
}
function closeAddTaskModal() {
document.getElementById('add-task-modal').classList.add('hidden');
}
function openAddUpdateModal() {
window.location.hash = 'updates-section';
}
</script> </script>
</body> </body>
</html> </html>