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');
}
}