diff --git a/Modules/Admin/Filament/Resources/UserResource.php b/Modules/Admin/Filament/Resources/UserResource.php index 580c26e65..e47db0ad9 100644 --- a/Modules/Admin/Filament/Resources/UserResource.php +++ b/Modules/Admin/Filament/Resources/UserResource.php @@ -1,21 +1,19 @@ schema([ - TextInput::make('name')->required()->maxLength(255), - TextInput::make('email')->email()->required()->maxLength(255)->unique(ignoreRecord: true), - TextInput::make('password')->password()->required(fn ($livewire) => $livewire instanceof Pages\CreateUser)->dehydrateStateUsing(fn ($state) => filled($state) ? bcrypt($state) : null)->dehydrated(fn ($state) => filled($state)), - StateFusionSelect::make('status')->required(), - Select::make('roles')->multiple()->relationship('roles', 'name')->preload(), + UserFormFields::name(), + UserFormFields::email(), + UserFormFields::password(fn ($livewire) => $livewire instanceof Pages\CreateUser), + UserFormFields::status(), + UserFormFields::roles(), ]); } diff --git a/Modules/Conversation/App/Http/Controllers/ConversationController.php b/Modules/Conversation/App/Http/Controllers/ConversationController.php new file mode 100644 index 000000000..9f98521a1 --- /dev/null +++ b/Modules/Conversation/App/Http/Controllers/ConversationController.php @@ -0,0 +1,120 @@ +user()->getKey(); + $messageFilter = $this->resolveMessageFilter($request); + + $conversations = Conversation::inboxForUser($userId, $messageFilter); + $selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation')); + + if ($selectedConversation) { + $selectedConversation->loadThread(); + $selectedConversation->markAsReadFor($userId); + + $conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation { + if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) { + $conversation->unread_count = 0; + } + + return $conversation; + }); + } + + return view('conversation::inbox', [ + 'conversations' => $conversations, + 'selectedConversation' => $selectedConversation, + 'messageFilter' => $messageFilter, + 'quickMessages' => QuickMessageCatalog::all(), + ]); + } + + public function start(Request $request, Listing $listing): RedirectResponse + { + $user = $request->user(); + + if (! $listing->user_id) { + return back()->with('error', 'Bu ilan için mesajlaşma açılamadı.'); + } + + if ((int) $listing->user_id === (int) $user->getKey()) { + return back()->with('error', 'Kendi ilanına mesaj gönderemezsin.'); + } + + $conversation = Conversation::openForListingBuyer($listing, (int) $user->getKey()); + + $user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]); + + $messageBody = trim((string) $request->string('message')); + + if ($messageBody !== '') { + $message = $conversation->messages()->create([ + 'sender_id' => $user->getKey(), + 'body' => $messageBody, + ]); + + $conversation->forceFill(['last_message_at' => $message->created_at])->save(); + } + + return redirect() + ->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()])) + ->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.'); + } + + public function send(Request $request, Conversation $conversation): RedirectResponse + { + $user = $request->user(); + $userId = (int) $user->getKey(); + + if ((int) $conversation->buyer_id !== $userId && (int) $conversation->seller_id !== $userId) { + abort(403); + } + + $payload = $request->validate([ + 'message' => ['required', 'string', 'max:2000'], + ]); + + $messageBody = trim($payload['message']); + + if ($messageBody === '') { + return back()->with('error', 'Mesaj boş olamaz.'); + } + + $message = $conversation->messages()->create([ + 'sender_id' => $userId, + 'body' => $messageBody, + ]); + + $conversation->forceFill(['last_message_at' => $message->created_at])->save(); + + return redirect() + ->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()])) + ->with('success', 'Mesaj gönderildi.'); + } + + private function inboxFilters(Request $request): array + { + $messageFilter = $this->resolveMessageFilter($request); + + return $messageFilter === 'all' ? [] : ['message_filter' => $messageFilter]; + } + + private function resolveMessageFilter(Request $request): string + { + $messageFilter = (string) $request->string('message_filter', 'all'); + + return in_array($messageFilter, ['all', 'unread', 'important'], true) ? $messageFilter : 'all'; + } +} diff --git a/Modules/Conversation/App/Models/Conversation.php b/Modules/Conversation/App/Models/Conversation.php new file mode 100644 index 000000000..e1611b363 --- /dev/null +++ b/Modules/Conversation/App/Models/Conversation.php @@ -0,0 +1,168 @@ + 'datetime']; + + public function listing() + { + return $this->belongsTo(Listing::class); + } + + public function seller() + { + return $this->belongsTo(User::class, 'seller_id'); + } + + public function buyer() + { + return $this->belongsTo(User::class, 'buyer_id'); + } + + public function messages() + { + return $this->hasMany(ConversationMessage::class); + } + + public function lastMessage() + { + return $this->hasOne(ConversationMessage::class) + ->latestOfMany() + ->select([ + 'conversation_messages.id', + 'conversation_messages.conversation_id', + 'conversation_messages.sender_id', + 'conversation_messages.body', + 'conversation_messages.created_at', + ]); + } + + public function scopeForUser(Builder $query, int $userId): Builder + { + return $query->where(function (Builder $participantQuery) use ($userId): void { + $participantQuery->where('buyer_id', $userId)->orWhere('seller_id', $userId); + }); + } + + public function scopeApplyMessageFilter(Builder $query, int $userId, string $messageFilter): Builder + { + if (! in_array($messageFilter, ['unread', 'important'], true)) { + return $query; + } + + return $query->whereHas('messages', function (Builder $messageQuery) use ($userId): void { + $messageQuery->where('sender_id', '!=', $userId)->whereNull('read_at'); + }); + } + + public function scopeWithInboxData(Builder $query, int $userId): Builder + { + return $query + ->with([ + 'listing:id,title,price,currency,user_id', + 'buyer:id,name', + 'seller:id,name', + 'lastMessage', + 'lastMessage.sender:id,name', + ]) + ->withCount([ + 'messages as unread_count' => fn (Builder $messageQuery) => $messageQuery + ->where('sender_id', '!=', $userId) + ->whereNull('read_at'), + ]) + ->orderByDesc('last_message_at') + ->orderByDesc('updated_at'); + } + + public static function inboxForUser(int $userId, string $messageFilter = 'all'): EloquentCollection + { + return static::query() + ->forUser($userId) + ->applyMessageFilter($userId, $messageFilter) + ->withInboxData($userId) + ->get(); + } + + public static function resolveSelected(EloquentCollection $conversations, ?int $conversationId): ?self + { + $selectedConversationId = $conversationId; + + if (($selectedConversationId ?? 0) <= 0 && $conversations->isNotEmpty()) { + $selectedConversationId = (int) $conversations->first()->getKey(); + } + + if (($selectedConversationId ?? 0) <= 0) { + return null; + } + + $selected = $conversations->firstWhere('id', $selectedConversationId); + + if (! $selected instanceof self) { + return null; + } + + return $selected; + } + + public function loadThread(): void + { + $this->load([ + 'listing:id,title,price,currency,user_id', + 'messages' => fn (Builder $query) => $query->with('sender:id,name')->orderBy('created_at'), + ]); + } + + public function markAsReadFor(int $userId): void + { + ConversationMessage::query() + ->where('conversation_id', $this->getKey()) + ->where('sender_id', '!=', $userId) + ->whereNull('read_at') + ->update([ + 'read_at' => now(), + 'updated_at' => now(), + ]); + } + + public static function openForListingBuyer(Listing $listing, int $buyerId): self + { + $conversation = static::query()->firstOrCreate( + [ + 'listing_id' => $listing->getKey(), + 'buyer_id' => $buyerId, + ], + [ + 'seller_id' => $listing->user_id, + ], + ); + + if ((int) $conversation->seller_id !== (int) $listing->user_id) { + $conversation->forceFill(['seller_id' => $listing->user_id])->save(); + } + + return $conversation; + } + + public static function buyerListingConversationId(int $listingId, int $buyerId): ?int + { + $value = static::query() + ->where('listing_id', $listingId) + ->where('buyer_id', $buyerId) + ->value('id'); + + return is_null($value) ? null : (int) $value; + } +} diff --git a/app/Models/ConversationMessage.php b/Modules/Conversation/App/Models/ConversationMessage.php similarity index 64% rename from app/Models/ConversationMessage.php rename to Modules/Conversation/App/Models/ConversationMessage.php index b26c99405..520b0124c 100644 --- a/app/Models/ConversationMessage.php +++ b/Modules/Conversation/App/Models/ConversationMessage.php @@ -1,24 +1,18 @@ 'datetime', - ]; + protected $casts = ['read_at' => 'datetime']; public function conversation() { diff --git a/Modules/Conversation/App/Providers/ConversationServiceProvider.php b/Modules/Conversation/App/Providers/ConversationServiceProvider.php new file mode 100644 index 000000000..73dbdb972 --- /dev/null +++ b/Modules/Conversation/App/Providers/ConversationServiceProvider.php @@ -0,0 +1,19 @@ +loadMigrationsFrom(module_path('Conversation', 'database/migrations')); + $this->loadRoutesFrom(module_path('Conversation', 'routes/web.php')); + $this->loadViewsFrom(module_path('Conversation', 'resources/views'), 'conversation'); + } + + public function register(): void + { + } +} diff --git a/Modules/Conversation/App/Support/QuickMessageCatalog.php b/Modules/Conversation/App/Support/QuickMessageCatalog.php new file mode 100644 index 000000000..7792a705d --- /dev/null +++ b/Modules/Conversation/App/Support/QuickMessageCatalog.php @@ -0,0 +1,16 @@ +id(); + $table->foreignId('listing_id')->constrained('listings')->cascadeOnDelete(); + $table->foreignId('seller_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('buyer_id')->constrained('users')->cascadeOnDelete(); + $table->timestamp('last_message_at')->nullable(); + $table->timestamps(); + + $table->unique(['listing_id', 'buyer_id']); + $table->index(['seller_id', 'last_message_at']); + $table->index(['buyer_id', 'last_message_at']); + }); + } + + if (! Schema::hasTable('conversation_messages')) { + Schema::create('conversation_messages', function (Blueprint $table): void { + $table->id(); + $table->foreignId('conversation_id')->constrained('conversations')->cascadeOnDelete(); + $table->foreignId('sender_id')->constrained('users')->cascadeOnDelete(); + $table->text('body'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + + $table->index(['conversation_id', 'created_at']); + $table->index(['conversation_id', 'read_at']); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('conversation_messages'); + Schema::dropIfExists('conversations'); + } +}; diff --git a/Modules/Conversation/module.json b/Modules/Conversation/module.json new file mode 100644 index 000000000..83a42dbf7 --- /dev/null +++ b/Modules/Conversation/module.json @@ -0,0 +1,11 @@ +{ + "name": "Conversation", + "alias": "conversation", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\Conversation\\App\\Providers\\ConversationServiceProvider" + ], + "files": [] +} diff --git a/resources/views/panel/inbox.blade.php b/Modules/Conversation/resources/views/inbox.blade.php similarity index 100% rename from resources/views/panel/inbox.blade.php rename to Modules/Conversation/resources/views/inbox.blade.php diff --git a/Modules/Conversation/routes/web.php b/Modules/Conversation/routes/web.php new file mode 100644 index 000000000..26114aedd --- /dev/null +++ b/Modules/Conversation/routes/web.php @@ -0,0 +1,13 @@ +prefix('panel')->name('panel.')->group(function () { + Route::get('/gelen-kutusu', [ConversationController::class, 'inbox'])->name('inbox.index'); +}); + +Route::middleware('auth')->name('conversations.')->group(function () { + Route::post('/listings/{listing}/conversation', [ConversationController::class, 'start'])->name('start'); + Route::post('/conversations/{conversation}/messages', [ConversationController::class, 'send'])->name('messages.send'); +}); diff --git a/app/Http/Controllers/FavoriteController.php b/Modules/Favorite/App/Http/Controllers/FavoriteController.php similarity index 57% rename from app/Http/Controllers/FavoriteController.php rename to Modules/Favorite/App/Http/Controllers/FavoriteController.php index c0c37269f..a9d1c9660 100644 --- a/app/Http/Controllers/FavoriteController.php +++ b/Modules/Favorite/App/Http/Controllers/FavoriteController.php @@ -1,43 +1,42 @@ string('tab', 'listings'); - if (! in_array($activeTab, ['listings', 'searches', 'sellers'], true)) { $activeTab = 'listings'; } $statusFilter = (string) $request->string('status', 'all'); - if (! in_array($statusFilter, ['all', 'active'], true)) { $statusFilter = 'all'; } - $selectedCategoryId = $request->integer('category'); - - if ($selectedCategoryId <= 0) { - $selectedCategoryId = null; - } - $messageFilter = (string) $request->string('message_filter', 'all'); if (! in_array($messageFilter, ['all', 'unread', 'important'], true)) { $messageFilter = 'all'; } + $selectedCategoryId = $request->integer('category'); + if ($selectedCategoryId <= 0) { + $selectedCategoryId = null; + } + $user = $request->user(); + $categories = Category::query() ->where('is_active', true) ->orderBy('name') @@ -49,12 +48,6 @@ class FavoriteController extends Controller $conversations = collect(); $selectedConversation = null; $buyerConversationListingMap = []; - $quickMessages = [ - 'Merhaba', - 'İlan hâlâ satışta mı?', - 'Son fiyat nedir?', - 'Teşekkürler', - ]; if ($activeTab === 'listings') { $favoriteListings = $user->favoriteListings() @@ -67,67 +60,26 @@ class FavoriteController extends Controller ->withQueryString(); $userId = (int) $user->getKey(); - $conversations = Conversation::query() - ->forUser($userId) - ->when(in_array($messageFilter, ['unread', 'important'], true), fn ($query) => $query->whereHas('messages', fn ($messageQuery) => $messageQuery - ->where('sender_id', '!=', $userId) - ->whereNull('read_at'))) - ->with([ - 'listing:id,title,price,currency,user_id', - 'buyer:id,name', - 'seller:id,name', - 'lastMessage', - 'lastMessage.sender:id,name', - ]) - ->withCount([ - 'messages as unread_count' => fn ($query) => $query - ->where('sender_id', '!=', $userId) - ->whereNull('read_at'), - ]) - ->orderByDesc('last_message_at') - ->orderByDesc('updated_at') - ->get(); - + $conversations = Conversation::inboxForUser($userId, $messageFilter); $buyerConversationListingMap = $conversations ->where('buyer_id', $userId) ->pluck('id', 'listing_id') ->map(fn ($conversationId) => (int) $conversationId) ->all(); - $selectedConversationId = $request->integer('conversation'); + $selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation')); - if ($selectedConversationId <= 0 && $conversations->isNotEmpty()) { - $selectedConversationId = (int) $conversations->first()->getKey(); - } + if ($selectedConversation) { + $selectedConversation->loadThread(); + $selectedConversation->markAsReadFor($userId); - if ($selectedConversationId > 0) { - $selectedConversation = $conversations->firstWhere('id', $selectedConversationId); + $conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation { + if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) { + $conversation->unread_count = 0; + } - if ($selectedConversation) { - $selectedConversation->load([ - 'listing:id,title,price,currency,user_id', - 'messages' => fn ($query) => $query - ->with('sender:id,name') - ->orderBy('created_at'), - ]); - - ConversationMessage::query() - ->where('conversation_id', $selectedConversation->getKey()) - ->where('sender_id', '!=', $userId) - ->whereNull('read_at') - ->update([ - 'read_at' => now(), - 'updated_at' => now(), - ]); - - $conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation { - if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) { - $conversation->unread_count = 0; - } - - return $conversation; - }); - } + return $conversation; + }); } } @@ -149,7 +101,7 @@ class FavoriteController extends Controller ->withQueryString(); } - return view('favorites.index', [ + return view('favorite::index', [ 'activeTab' => $activeTab, 'statusFilter' => $statusFilter, 'selectedCategoryId' => $selectedCategoryId, @@ -161,24 +113,15 @@ class FavoriteController extends Controller 'conversations' => $conversations, 'selectedConversation' => $selectedConversation, 'buyerConversationListingMap' => $buyerConversationListingMap, - 'quickMessages' => $quickMessages, + 'quickMessages' => QuickMessageCatalog::all(), ]); } public function toggleListing(Request $request, Listing $listing) { - $user = $request->user(); - $isFavorite = $user->favoriteListings()->whereKey($listing->getKey())->exists(); + $isNowFavorite = $request->user()->toggleFavoriteListing($listing); - if ($isFavorite) { - $user->favoriteListings()->detach($listing->getKey()); - - return back()->with('success', 'İlan favorilerden kaldırıldı.'); - } - - $user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]); - - return back()->with('success', 'İlan favorilere eklendi.'); + return back()->with('success', $isNowFavorite ? 'İlan favorilere eklendi.' : 'İlan favorilerden kaldırıldı.'); } public function toggleSeller(Request $request, User $seller) @@ -189,17 +132,9 @@ class FavoriteController extends Controller return back()->with('error', 'Kendi hesabını favorilere ekleyemezsin.'); } - $isFavorite = $user->favoriteSellers()->whereKey($seller->getKey())->exists(); + $isNowFavorite = $user->toggleFavoriteSeller($seller); - if ($isFavorite) { - $user->favoriteSellers()->detach($seller->getKey()); - - return back()->with('success', 'Satıcı favorilerden kaldırıldı.'); - } - - $user->favoriteSellers()->syncWithoutDetaching([$seller->getKey()]); - - return back()->with('success', 'Satıcı favorilere eklendi.'); + return back()->with('success', $isNowFavorite ? 'Satıcı favorilere eklendi.' : 'Satıcı favorilerden kaldırıldı.'); } public function storeSearch(Request $request) @@ -225,15 +160,7 @@ class FavoriteController extends Controller $categoryName = Category::query()->whereKey($filters['category'])->value('name'); } - $labelParts = []; - if (! empty($filters['search'])) { - $labelParts[] = '"'.$filters['search'].'"'; - } - if ($categoryName) { - $labelParts[] = $categoryName; - } - - $label = $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtreli arama'; + $label = FavoriteSearch::labelFor($filters, is_string($categoryName) ? $categoryName : null); $favoriteSearch = $request->user()->favoriteSearches()->firstOrCreate( ['signature' => $signature], diff --git a/app/Models/FavoriteSearch.php b/Modules/Favorite/App/Models/FavoriteSearch.php similarity index 52% rename from app/Models/FavoriteSearch.php rename to Modules/Favorite/App/Models/FavoriteSearch.php index fc0eecea4..07130acc1 100644 --- a/app/Models/FavoriteSearch.php +++ b/Modules/Favorite/App/Models/FavoriteSearch.php @@ -1,23 +1,16 @@ 'array', - ]; + protected $casts = ['filters' => 'array']; public function user() { @@ -26,7 +19,7 @@ class FavoriteSearch extends Model public function category() { - return $this->belongsTo(\Modules\Category\Models\Category::class); + return $this->belongsTo(Category::class); } public static function normalizeFilters(array $filters): array @@ -45,4 +38,19 @@ class FavoriteSearch extends Model return hash('sha256', is_string($payload) ? $payload : ''); } + + public static function labelFor(array $filters, ?string $categoryName = null): string + { + $labelParts = []; + + if (! empty($filters['search'])) { + $labelParts[] = '"'.$filters['search'].'"'; + } + + if (filled($categoryName)) { + $labelParts[] = $categoryName; + } + + return $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtreli arama'; + } } diff --git a/Modules/Favorite/App/Providers/FavoriteServiceProvider.php b/Modules/Favorite/App/Providers/FavoriteServiceProvider.php new file mode 100644 index 000000000..3e456d801 --- /dev/null +++ b/Modules/Favorite/App/Providers/FavoriteServiceProvider.php @@ -0,0 +1,19 @@ +loadMigrationsFrom(module_path('Favorite', 'database/migrations')); + $this->loadRoutesFrom(module_path('Favorite', 'routes/web.php')); + $this->loadViewsFrom(module_path('Favorite', 'resources/views'), 'favorite'); + } + + public function register(): void + { + } +} diff --git a/Modules/Favorite/database/migrations/2026_03_04_000000_create_favorites_tables.php b/Modules/Favorite/database/migrations/2026_03_04_000000_create_favorites_tables.php new file mode 100644 index 000000000..23995ff2a --- /dev/null +++ b/Modules/Favorite/database/migrations/2026_03_04_000000_create_favorites_tables.php @@ -0,0 +1,55 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('listing_id')->constrained('listings')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['user_id', 'listing_id']); + }); + } + + if (! Schema::hasTable('favorite_sellers')) { + Schema::create('favorite_sellers', function (Blueprint $table): void { + $table->id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('seller_id')->constrained('users')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['user_id', 'seller_id']); + }); + } + + if (! Schema::hasTable('favorite_searches')) { + Schema::create('favorite_searches', function (Blueprint $table): void { + $table->id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('label')->nullable(); + $table->string('search_term')->nullable(); + $table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete(); + $table->json('filters')->nullable(); + $table->string('signature', 64); + $table->timestamps(); + + $table->unique(['user_id', 'signature']); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('favorite_searches'); + Schema::dropIfExists('favorite_sellers'); + Schema::dropIfExists('favorite_listings'); + } +}; diff --git a/Modules/Favorite/module.json b/Modules/Favorite/module.json new file mode 100644 index 000000000..3433a1588 --- /dev/null +++ b/Modules/Favorite/module.json @@ -0,0 +1,11 @@ +{ + "name": "Favorite", + "alias": "favorite", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\Favorite\\App\\Providers\\FavoriteServiceProvider" + ], + "files": [] +} diff --git a/resources/views/favorites/index.blade.php b/Modules/Favorite/resources/views/index.blade.php similarity index 100% rename from resources/views/favorites/index.blade.php rename to Modules/Favorite/resources/views/index.blade.php diff --git a/Modules/Favorite/routes/web.php b/Modules/Favorite/routes/web.php new file mode 100644 index 000000000..c29819b53 --- /dev/null +++ b/Modules/Favorite/routes/web.php @@ -0,0 +1,12 @@ +prefix('favorites')->name('favorites.')->group(function () { + Route::get('/', [FavoriteController::class, 'index'])->name('index'); + Route::post('/listings/{listing}/toggle', [FavoriteController::class, 'toggleListing'])->name('listings.toggle'); + Route::post('/sellers/{seller}/toggle', [FavoriteController::class, 'toggleSeller'])->name('sellers.toggle'); + Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store'); + Route::delete('/searches/{favoriteSearch}', [FavoriteController::class, 'destroySearch'])->name('searches.destroy'); +}); diff --git a/Modules/Listing/Database/Seeders/ListingSeeder.php b/Modules/Listing/Database/Seeders/ListingSeeder.php index 6f6d6663a..531512766 100644 --- a/Modules/Listing/Database/Seeders/ListingSeeder.php +++ b/Modules/Listing/Database/Seeders/ListingSeeder.php @@ -2,7 +2,7 @@ namespace Modules\Listing\Database\Seeders; -use App\Models\User; +use Modules\User\App\Models\User; use Illuminate\Database\Seeder; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -11,9 +11,6 @@ use Modules\Listing\Models\Listing; class ListingSeeder extends Seeder { - /** - * @var array - */ private const LISTINGS = [ [ 'title' => 'iPhone 14 Pro 256 GB, temiz kullanılmış', @@ -106,9 +103,6 @@ class ListingSeeder extends Seeder ->first(); } - /** - * @param array{title: string, description: string, price: int, city: string, country: string, image: string} $data - */ private function upsertListing(int $index, array $data, Collection $categories, User $user): Listing { $slug = Str::slug($data['title']) . '-' . ($index + 1); @@ -167,7 +161,11 @@ class ListingSeeder extends Seeder $media = $mediaItems->first(); - if (! $media || (string) $media->file_name !== $targetFileName) { + if ( + ! $media + || (string) $media->file_name !== $targetFileName + || (string) $media->disk !== 'public' + ) { return false; } diff --git a/Modules/Listing/Http/Controllers/ListingController.php b/Modules/Listing/Http/Controllers/ListingController.php index 6e99d97da..51e93acb8 100644 --- a/Modules/Listing/Http/Controllers/ListingController.php +++ b/Modules/Listing/Http/Controllers/ListingController.php @@ -2,8 +2,8 @@ namespace Modules\Listing\Http\Controllers; use App\Http\Controllers\Controller; -use App\Models\Conversation; -use App\Models\FavoriteSearch; +use Modules\Conversation\App\Models\Conversation; +use Modules\Favorite\App\Models\FavoriteSearch; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Schema; diff --git a/Modules/Listing/Models/Listing.php b/Modules/Listing/Models/Listing.php index 4a88b3ba6..1ac88d7d5 100644 --- a/Modules/Listing/Models/Listing.php +++ b/Modules/Listing/Models/Listing.php @@ -55,18 +55,18 @@ class Listing extends Model implements HasMedia public function user() { - return $this->belongsTo(\App\Models\User::class); + return $this->belongsTo(\Modules\User\App\Models\User::class); } public function favoritedByUsers() { - return $this->belongsToMany(\App\Models\User::class, 'favorite_listings') + return $this->belongsToMany(\Modules\User\App\Models\User::class, 'favorite_listings') ->withTimestamps(); } public function conversations() { - return $this->hasMany(\App\Models\Conversation::class); + return $this->hasMany(\Modules\Conversation\App\Models\Conversation::class); } public function scopePublicFeed(Builder $query): Builder diff --git a/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php b/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php index bac76a9e6..b34e56e31 100644 --- a/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php +++ b/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php @@ -20,7 +20,7 @@ use Modules\Listing\Support\ListingPanelHelper; use Modules\Location\Models\City; use Modules\Location\Models\Country; use Modules\Partner\Filament\Resources\ListingResource; -use Modules\Profile\Models\Profile; +use Modules\User\App\Models\Profile; use Throwable; class QuickCreateListing extends Page diff --git a/Modules/Partner/Providers/PartnerPanelProvider.php b/Modules/Partner/Providers/PartnerPanelProvider.php index f31c2b29a..f681d7fd2 100644 --- a/Modules/Partner/Providers/PartnerPanelProvider.php +++ b/Modules/Partner/Providers/PartnerPanelProvider.php @@ -2,7 +2,7 @@ namespace Modules\Partner\Providers; use A909M\FilamentStateFusion\FilamentStateFusionPlugin; -use App\Models\User; +use Modules\User\App\Models\User; use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin; use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use Filament\Http\Middleware\Authenticate; diff --git a/Modules/Profile/Http/Controllers/ProfileController.php b/Modules/Profile/Http/Controllers/ProfileController.php deleted file mode 100644 index b33b8266e..000000000 --- a/Modules/Profile/Http/Controllers/ProfileController.php +++ /dev/null @@ -1,34 +0,0 @@ - auth()->id()]); - return view('profile::show', compact('profile')); - } - - public function edit() - { - $profile = Profile::firstOrCreate(['user_id' => auth()->id()]); - return view('profile::edit', compact('profile')); - } - - public function update(Request $request) - { - $data = $request->validate([ - 'bio' => 'nullable|string|max:500', - 'phone' => 'nullable|string|max:20', - 'city' => 'nullable|string|max:100', - 'country' => 'nullable|string|max:100', - 'website' => 'nullable|url', - ]); - Profile::updateOrCreate(['user_id' => auth()->id()], $data); - return redirect()->route('profile.show')->with('success', 'Profile updated!'); - } -} diff --git a/Modules/Profile/Providers/ProfileServiceProvider.php b/Modules/Profile/Providers/ProfileServiceProvider.php deleted file mode 100644 index ab9f10574..000000000 --- a/Modules/Profile/Providers/ProfileServiceProvider.php +++ /dev/null @@ -1,18 +0,0 @@ -loadMigrationsFrom(module_path($this->moduleName, 'database/migrations')); - $this->loadRoutesFrom(module_path($this->moduleName, 'routes/web.php')); - $this->loadViewsFrom(module_path($this->moduleName, 'resources/views'), 'profile'); - } - - public function register(): void {} -} diff --git a/Modules/Profile/database/migrations/2024_01_01_000005_create_profiles_table.php b/Modules/Profile/database/migrations/2024_01_01_000005_create_profiles_table.php deleted file mode 100644 index 9925e6130..000000000 --- a/Modules/Profile/database/migrations/2024_01_01_000005_create_profiles_table.php +++ /dev/null @@ -1,28 +0,0 @@ -id(); - $table->foreignId('user_id')->unique()->constrained('users')->cascadeOnDelete(); - $table->string('avatar')->nullable(); - $table->text('bio')->nullable(); - $table->string('phone')->nullable(); - $table->string('city')->nullable(); - $table->string('country')->nullable(); - $table->string('website')->nullable(); - $table->boolean('is_verified')->default(false); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('profiles'); - } -}; diff --git a/Modules/Profile/module.json b/Modules/Profile/module.json deleted file mode 100644 index 7fe813fb2..000000000 --- a/Modules/Profile/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Profile", - "alias": "profile", - "description": "User profile management", - "keywords": [], - "priority": 0, - "providers": [ - "Modules\\Profile\\Providers\\ProfileServiceProvider" - ], - "aliases": {}, - "files": [] -} diff --git a/Modules/Profile/resources/views/edit.blade.php b/Modules/Profile/resources/views/edit.blade.php deleted file mode 100644 index dd96815fb..000000000 --- a/Modules/Profile/resources/views/edit.blade.php +++ /dev/null @@ -1,34 +0,0 @@ -@extends('app::layouts.app') -@section('content') -
-
-

Edit Profile

-
- @csrf @method('PUT') -
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- - -
- -
-
-
-@endsection diff --git a/Modules/Profile/resources/views/show.blade.php b/Modules/Profile/resources/views/show.blade.php deleted file mode 100644 index 24439e2eb..000000000 --- a/Modules/Profile/resources/views/show.blade.php +++ /dev/null @@ -1,25 +0,0 @@ -@extends('app::layouts.app') -@section('content') -
-
-
-
- {{ substr(auth()->user()->name, 0, 1) }} -
-
-

{{ auth()->user()->name }}

-

{{ auth()->user()->email }}

-
-
- @if($profile->bio)

{{ $profile->bio }}

@endif -
- @if($profile->phone)

📞 {{ $profile->phone }}

@endif - @if($profile->city)

📍 {{ $profile->city }}@if($profile->country), {{ $profile->country }}@endif

@endif - @if($profile->website)

🌐 {{ $profile->website }}

@endif -
- -
-
-@endsection diff --git a/Modules/Profile/routes/web.php b/Modules/Profile/routes/web.php deleted file mode 100644 index 3b202e1e3..000000000 --- a/Modules/Profile/routes/web.php +++ /dev/null @@ -1,9 +0,0 @@ -prefix('profile')->name('profile.')->group(function () { - Route::get('/', [ProfileController::class, 'show'])->name('show'); - Route::get('/edit', [ProfileController::class, 'edit'])->name('edit'); - Route::put('/extended', [ProfileController::class, 'update'])->name('update'); -}); diff --git a/app/Http/Controllers/Auth/ProfileController.php b/Modules/User/App/Http/Controllers/ProfileController.php similarity index 66% rename from app/Http/Controllers/Auth/ProfileController.php rename to Modules/User/App/Http/Controllers/ProfileController.php index 6ab3c68eb..63a68c198 100644 --- a/app/Http/Controllers/Auth/ProfileController.php +++ b/Modules/User/App/Http/Controllers/ProfileController.php @@ -1,23 +1,33 @@ $request->user()]); + } + public function update(Request $request): RedirectResponse { $validated = $request->validateWithBag('updateProfile', [ 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique('users')->ignore($request->user()->id)], + 'email' => [ + 'required', + 'string', + 'lowercase', + 'email', + 'max:255', + Rule::unique('users')->ignore($request->user()->id), + ], ]); $request->user()->fill($validated); @@ -28,12 +38,9 @@ class ProfileController extends Controller $request->user()->save(); - return redirect('/profile')->with('status', 'profile-updated'); + return redirect()->route('profile.edit')->with('status', 'profile-updated'); } - /** - * Delete the user's account. - */ public function destroy(Request $request): RedirectResponse { $request->validateWithBag('userDeletion', [ @@ -43,7 +50,6 @@ class ProfileController extends Controller $user = $request->user(); Auth::logout(); - $user->delete(); $request->session()->invalidate(); diff --git a/Modules/Profile/Models/Profile.php b/Modules/User/App/Models/Profile.php similarity index 86% rename from Modules/Profile/Models/Profile.php rename to Modules/User/App/Models/Profile.php index 8283751cc..1528f408f 100644 --- a/Modules/Profile/Models/Profile.php +++ b/Modules/User/App/Models/Profile.php @@ -1,5 +1,6 @@ 'boolean']; public function getActivitylogOptions(): LogOptions @@ -22,6 +24,6 @@ class Profile extends Model public function user() { - return $this->belongsTo(\App\Models\User::class); + return $this->belongsTo(User::class); } } diff --git a/app/Models/User.php b/Modules/User/App/Models/User.php similarity index 59% rename from app/Models/User.php rename to Modules/User/App/Models/User.php index 2c97ad261..705299105 100644 --- a/app/Models/User.php +++ b/Modules/User/App/Models/User.php @@ -1,11 +1,12 @@ hasMany(\Modules\Listing\Models\Listing::class); + return $this->hasMany(Listing::class); + } + + public function profile() + { + return $this->hasOne(Profile::class); } public function favoriteListings() { - return $this->belongsToMany(\Modules\Listing\Models\Listing::class, 'favorite_listings') - ->withTimestamps(); + return $this->belongsToMany(Listing::class, 'favorite_listings')->withTimestamps(); } public function favoriteSellers() { - return $this->belongsToMany(self::class, 'favorite_sellers', 'user_id', 'seller_id') - ->withTimestamps(); + return $this->belongsToMany(self::class, 'favorite_sellers', 'user_id', 'seller_id')->withTimestamps(); } public function favoriteSearches() @@ -111,8 +132,40 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata public function getFilamentAvatarUrl(): ?string { - return filled($this->avatar_url) - ? Storage::disk('public')->url($this->avatar_url) - : null; + return filled($this->avatar_url) ? Storage::disk('public')->url($this->avatar_url) : null; + } + + public function toggleFavoriteListing(Listing $listing): bool + { + $isFavorite = $this->favoriteListings()->whereKey($listing->getKey())->exists(); + + if ($isFavorite) { + $this->favoriteListings()->detach($listing->getKey()); + + return false; + } + + $this->favoriteListings()->syncWithoutDetaching([$listing->getKey()]); + + return true; + } + + public function toggleFavoriteSeller(self $seller): bool + { + if ((int) $seller->getKey() === (int) $this->getKey()) { + return false; + } + + $isFavorite = $this->favoriteSellers()->whereKey($seller->getKey())->exists(); + + if ($isFavorite) { + $this->favoriteSellers()->detach($seller->getKey()); + + return false; + } + + $this->favoriteSellers()->syncWithoutDetaching([$seller->getKey()]); + + return true; } } diff --git a/Modules/User/App/Providers/UserServiceProvider.php b/Modules/User/App/Providers/UserServiceProvider.php new file mode 100644 index 000000000..40f564b8e --- /dev/null +++ b/Modules/User/App/Providers/UserServiceProvider.php @@ -0,0 +1,19 @@ +loadMigrationsFrom(module_path('User', 'database/migrations')); + $this->loadRoutesFrom(module_path('User', 'routes/web.php')); + $this->loadViewsFrom(module_path('User', 'resources/views'), 'user'); + } + + public function register(): void + { + } +} diff --git a/app/States/ActiveUserStatus.php b/Modules/User/App/States/ActiveUserStatus.php similarity index 95% rename from app/States/ActiveUserStatus.php rename to Modules/User/App/States/ActiveUserStatus.php index 545acbaad..08b73d2ed 100644 --- a/app/States/ActiveUserStatus.php +++ b/Modules/User/App/States/ActiveUserStatus.php @@ -1,6 +1,6 @@ required()->maxLength(255); + } + + public static function email(): TextInput + { + return TextInput::make('email')->email()->required()->maxLength(255)->unique(ignoreRecord: true); + } + + public static function password(callable $requiredCallback): TextInput + { + return TextInput::make('password') + ->password() + ->required($requiredCallback) + ->dehydrateStateUsing(fn ($state) => filled($state) ? bcrypt($state) : null) + ->dehydrated(fn ($state) => filled($state)); + } + + public static function status(): StateFusionSelect + { + return StateFusionSelect::make('status')->required(); + } + + public static function roles(): Select + { + return Select::make('roles')->multiple()->relationship('roles', 'name')->preload(); + } +} diff --git a/Modules/User/database/migrations/0001_01_01_000000_create_user_domain_tables.php b/Modules/User/database/migrations/0001_01_01_000000_create_user_domain_tables.php new file mode 100644 index 000000000..37ad3c0f8 --- /dev/null +++ b/Modules/User/database/migrations/0001_01_01_000000_create_user_domain_tables.php @@ -0,0 +1,67 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('status')->default('active'); + $table->string('password'); + $table->string('avatar_url')->nullable(); + $table->rememberToken(); + $table->timestamps(); + }); + } + + if (! Schema::hasTable('profiles')) { + Schema::create('profiles', function (Blueprint $table): void { + $table->id(); + $table->foreignId('user_id')->unique()->constrained('users')->cascadeOnDelete(); + $table->string('avatar')->nullable(); + $table->text('bio')->nullable(); + $table->string('phone')->nullable(); + $table->string('city')->nullable(); + $table->string('country')->nullable(); + $table->string('website')->nullable(); + $table->boolean('is_verified')->default(false); + $table->timestamps(); + }); + } + + if (! Schema::hasTable('password_reset_tokens')) { + Schema::create('password_reset_tokens', function (Blueprint $table): void { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } + + if (! Schema::hasTable('sessions')) { + Schema::create('sessions', function (Blueprint $table): void { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('sessions'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('profiles'); + Schema::dropIfExists('users'); + } +}; diff --git a/Modules/User/module.json b/Modules/User/module.json new file mode 100644 index 000000000..9a1f82717 --- /dev/null +++ b/Modules/User/module.json @@ -0,0 +1,11 @@ +{ + "name": "User", + "alias": "user", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\User\\App\\Providers\\UserServiceProvider" + ], + "files": [] +} diff --git a/resources/views/profile/edit.blade.php b/Modules/User/resources/views/profile/edit.blade.php similarity index 76% rename from resources/views/profile/edit.blade.php rename to Modules/User/resources/views/profile/edit.blade.php index e0e1d387e..c3fdb0050 100644 --- a/resources/views/profile/edit.blade.php +++ b/Modules/User/resources/views/profile/edit.blade.php @@ -9,19 +9,19 @@
- @include('profile.partials.update-profile-information-form') + @include('user::profile.partials.update-profile-information-form')
- @include('profile.partials.update-password-form') + @include('user::profile.partials.update-password-form')
- @include('profile.partials.delete-user-form') + @include('user::profile.partials.delete-user-form')
diff --git a/resources/views/profile/partials/delete-user-form.blade.php b/Modules/User/resources/views/profile/partials/delete-user-form.blade.php similarity index 100% rename from resources/views/profile/partials/delete-user-form.blade.php rename to Modules/User/resources/views/profile/partials/delete-user-form.blade.php diff --git a/resources/views/profile/partials/update-password-form.blade.php b/Modules/User/resources/views/profile/partials/update-password-form.blade.php similarity index 100% rename from resources/views/profile/partials/update-password-form.blade.php rename to Modules/User/resources/views/profile/partials/update-password-form.blade.php diff --git a/resources/views/profile/partials/update-profile-information-form.blade.php b/Modules/User/resources/views/profile/partials/update-profile-information-form.blade.php similarity index 100% rename from resources/views/profile/partials/update-profile-information-form.blade.php rename to Modules/User/resources/views/profile/partials/update-profile-information-form.blade.php diff --git a/Modules/User/routes/web.php b/Modules/User/routes/web.php new file mode 100644 index 000000000..3dd5cde7f --- /dev/null +++ b/Modules/User/routes/web.php @@ -0,0 +1,10 @@ +prefix('profile')->name('profile.')->group(function () { + Route::get('/', [ProfileController::class, 'edit'])->name('edit'); + Route::patch('/', [ProfileController::class, 'update'])->name('update'); + Route::delete('/', [ProfileController::class, 'destroy'])->name('destroy'); +}); diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php index e8368bd22..afbf46467 100644 --- a/app/Http/Controllers/Auth/NewPasswordController.php +++ b/app/Http/Controllers/Auth/NewPasswordController.php @@ -3,7 +3,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Models\User; +use Modules\User\App\Models\User; use Illuminate\Auth\Events\PasswordReset; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -15,19 +15,11 @@ use Illuminate\View\View; class NewPasswordController extends Controller { - /** - * Display the password reset view. - */ public function create(Request $request): View { return view('auth.reset-password', ['request' => $request]); } - /** - * Handle an incoming new password request. - * - * @throws \Illuminate\Validation\ValidationException - */ public function store(Request $request): RedirectResponse { $request->validate([ @@ -36,9 +28,6 @@ class NewPasswordController extends Controller 'password' => ['required', 'confirmed', Rules\Password::defaults()], ]); - // Here we will attempt to reset the user's password. If it is successful we - // will update the password on an actual user model and persist it to the - // database. Otherwise we will parse the error and return the response. $status = Password::reset( $request->only('email', 'password', 'password_confirmation', 'token'), function (User $user) use ($request) { @@ -51,9 +40,6 @@ class NewPasswordController extends Controller } ); - // If the password was successfully reset, we will redirect the user back to - // the application's home authenticated view. If there is an error we can - // redirect them back to where they came from with their error message. return $status == Password::PASSWORD_RESET ? redirect()->route('login')->with('status', __($status)) : back()->withInput($request->only('email')) diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 0739e2e87..a478e9c5a 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -3,7 +3,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Models\User; +use Modules\User\App\Models\User; use Illuminate\Auth\Events\Registered; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -14,19 +14,11 @@ use Illuminate\View\View; class RegisteredUserController extends Controller { - /** - * Display the registration view. - */ public function create(): View { return view('auth.register'); } - /** - * Handle an incoming registration request. - * - * @throws \Illuminate\Validation\ValidationException - */ public function store(Request $request): RedirectResponse { $request->validate([ diff --git a/app/Http/Controllers/Auth/SocialAuthController.php b/app/Http/Controllers/Auth/SocialAuthController.php index 60bf340d3..4a5675bcf 100644 --- a/app/Http/Controllers/Auth/SocialAuthController.php +++ b/app/Http/Controllers/Auth/SocialAuthController.php @@ -3,7 +3,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Models\User; +use Modules\User\App\Models\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -15,9 +15,6 @@ use Throwable; class SocialAuthController extends Controller { - /** - * @var array - */ private array $allowedProviders = ['google', 'facebook', 'apple']; public function redirect(string $provider): RedirectResponse diff --git a/app/Http/Controllers/ConversationController.php b/app/Http/Controllers/ConversationController.php deleted file mode 100644 index 25ca9bb94..000000000 --- a/app/Http/Controllers/ConversationController.php +++ /dev/null @@ -1,104 +0,0 @@ -user(); - - if (! $listing->user_id) { - return back()->with('error', 'Bu ilan için mesajlaşma açılamadı.'); - } - - if ((int) $listing->user_id === (int) $user->getKey()) { - return back()->with('error', 'Kendi ilanına mesaj gönderemezsin.'); - } - - $conversation = Conversation::query()->firstOrCreate( - [ - 'listing_id' => $listing->getKey(), - 'buyer_id' => $user->getKey(), - ], - [ - 'seller_id' => $listing->user_id, - ], - ); - - if ((int) $conversation->seller_id !== (int) $listing->user_id) { - $conversation->forceFill([ - 'seller_id' => $listing->user_id, - ])->save(); - } - - $user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]); - - $messageBody = trim((string) $request->string('message')); - - if ($messageBody !== '') { - $message = $conversation->messages()->create([ - 'sender_id' => $user->getKey(), - 'body' => $messageBody, - ]); - - $conversation->forceFill([ - 'last_message_at' => $message->created_at, - ])->save(); - } - - return redirect() - ->route('panel.inbox.index', array_merge( - $this->inboxFilters($request), - ['conversation' => $conversation->getKey()], - )) - ->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.'); - } - - public function send(Request $request, Conversation $conversation): RedirectResponse - { - $user = $request->user(); - $userId = (int) $user->getKey(); - - if ((int) $conversation->buyer_id !== $userId && (int) $conversation->seller_id !== $userId) { - abort(403); - } - - $payload = $request->validate([ - 'message' => ['required', 'string', 'max:2000'], - ]); - - $message = $conversation->messages()->create([ - 'sender_id' => $userId, - 'body' => trim($payload['message']), - ]); - - $conversation->forceFill([ - 'last_message_at' => $message->created_at, - ])->save(); - - return redirect() - ->route('panel.inbox.index', array_merge( - $this->inboxFilters($request), - ['conversation' => $conversation->getKey()], - )) - ->with('success', 'Mesaj gönderildi.'); - } - - private function inboxFilters(Request $request): array - { - $filters = []; - - $messageFilter = (string) $request->string('message_filter'); - if (in_array($messageFilter, ['all', 'unread', 'important'], true)) { - $filters['message_filter'] = $messageFilter; - } - - return $filters; - } -} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 4e3698432..240397309 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Modules\Listing\Models\Listing; use Modules\Category\Models\Category; -use App\Models\User; +use Modules\User\App\Models\User; class HomeController extends Controller { diff --git a/app/Http/Controllers/PanelController.php b/app/Http/Controllers/PanelController.php index 0f824e25f..06ca91b35 100644 --- a/app/Http/Controllers/PanelController.php +++ b/app/Http/Controllers/PanelController.php @@ -2,8 +2,6 @@ namespace App\Http\Controllers; -use App\Models\Conversation; -use App\Models\ConversationMessage; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -59,88 +57,6 @@ class PanelController extends Controller ]); } - public function inbox(Request $request): View - { - $userId = (int) $request->user()->getKey(); - - $messageFilter = (string) $request->string('message_filter', 'all'); - if (! in_array($messageFilter, ['all', 'unread', 'important'], true)) { - $messageFilter = 'all'; - } - - $conversations = Conversation::query() - ->forUser($userId) - ->when( - in_array($messageFilter, ['unread', 'important'], true), - fn ($query) => $query->whereHas('messages', fn ($messageQuery) => $messageQuery - ->where('sender_id', '!=', $userId) - ->whereNull('read_at')) - ) - ->with([ - 'listing:id,title,price,currency,user_id', - 'buyer:id,name', - 'seller:id,name', - 'lastMessage', - ]) - ->withCount([ - 'messages as unread_count' => fn ($query) => $query - ->where('sender_id', '!=', $userId) - ->whereNull('read_at'), - ]) - ->orderByDesc('last_message_at') - ->orderByDesc('updated_at') - ->get(); - - $selectedConversation = null; - $selectedConversationId = $request->integer('conversation'); - - if ($selectedConversationId <= 0 && $conversations->isNotEmpty()) { - $selectedConversationId = (int) $conversations->first()->getKey(); - } - - if ($selectedConversationId > 0) { - $selectedConversation = $conversations->firstWhere('id', $selectedConversationId); - - if ($selectedConversation) { - $selectedConversation->load([ - 'listing:id,title,price,currency,user_id', - 'messages' => fn ($query) => $query - ->with('sender:id,name') - ->orderBy('created_at'), - ]); - - ConversationMessage::query() - ->where('conversation_id', $selectedConversation->getKey()) - ->where('sender_id', '!=', $userId) - ->whereNull('read_at') - ->update([ - 'read_at' => now(), - 'updated_at' => now(), - ]); - - $conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation { - if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) { - $conversation->unread_count = 0; - } - - return $conversation; - }); - } - } - - return view('panel.inbox', [ - 'conversations' => $conversations, - 'selectedConversation' => $selectedConversation, - 'messageFilter' => $messageFilter, - 'quickMessages' => [ - 'Merhaba', - 'İlan hâlâ satışta mı?', - 'Son fiyat nedir?', - 'Teşekkürler', - ], - ]); - } - public function destroyListing(Request $request, Listing $listing): RedirectResponse { $this->guardListingOwner($request, $listing); diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php deleted file mode 100644 index a48eb8d82..000000000 --- a/app/Http/Controllers/ProfileController.php +++ /dev/null @@ -1,60 +0,0 @@ - $request->user(), - ]); - } - - /** - * Update the user's profile information. - */ - public function update(ProfileUpdateRequest $request): RedirectResponse - { - $request->user()->fill($request->validated()); - - if ($request->user()->isDirty('email')) { - $request->user()->email_verified_at = null; - } - - $request->user()->save(); - - return Redirect::route('profile.edit')->with('status', 'profile-updated'); - } - - /** - * Delete the user's account. - */ - public function destroy(Request $request): RedirectResponse - { - $request->validateWithBag('userDeletion', [ - 'password' => ['required', 'current_password'], - ]); - - $user = $request->user(); - - Auth::logout(); - - $user->delete(); - - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - return Redirect::to('/'); - } -} diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php deleted file mode 100644 index 3622a8f37..000000000 --- a/app/Http/Requests/ProfileUpdateRequest.php +++ /dev/null @@ -1,30 +0,0 @@ -|string> - */ - public function rules(): array - { - return [ - 'name' => ['required', 'string', 'max:255'], - 'email' => [ - 'required', - 'string', - 'lowercase', - 'email', - 'max:255', - Rule::unique(User::class)->ignore($this->user()->id), - ], - ]; - } -} diff --git a/app/Livewire/PanelQuickListingForm.php b/app/Livewire/PanelQuickListingForm.php index fdb545687..600b3f43e 100644 --- a/app/Livewire/PanelQuickListingForm.php +++ b/app/Livewire/PanelQuickListingForm.php @@ -16,7 +16,7 @@ use Modules\Listing\Support\ListingCustomFieldSchemaBuilder; use Modules\Listing\Support\ListingPanelHelper; use Modules\Location\Models\City; use Modules\Location\Models\Country; -use Modules\Profile\Models\Profile; +use Modules\User\App\Models\Profile; use Throwable; class PanelQuickListingForm extends Component diff --git a/app/Models/Conversation.php b/app/Models/Conversation.php deleted file mode 100644 index e8038dcee..000000000 --- a/app/Models/Conversation.php +++ /dev/null @@ -1,76 +0,0 @@ - 'datetime', - ]; - - public function listing() - { - return $this->belongsTo(Listing::class); - } - - public function seller() - { - return $this->belongsTo(User::class, 'seller_id'); - } - - public function buyer() - { - return $this->belongsTo(User::class, 'buyer_id'); - } - - public function messages() - { - return $this->hasMany(ConversationMessage::class); - } - - public function lastMessage() - { - return $this->hasOne(ConversationMessage::class) - ->latestOfMany() - ->select([ - 'conversation_messages.id', - 'conversation_messages.conversation_id', - 'conversation_messages.sender_id', - 'conversation_messages.body', - 'conversation_messages.created_at', - ]); - } - - public function scopeForUser(Builder $query, int $userId): Builder - { - return $query->where(function (Builder $participantQuery) use ($userId): void { - $participantQuery - ->where('buyer_id', $userId) - ->orWhere('seller_id', $userId); - }); - } - - public static function buyerListingConversationId(int $listingId, int $buyerId): ?int - { - $value = static::query() - ->where('listing_id', $listingId) - ->where('buyer_id', $buyerId) - ->value('id'); - - return is_null($value) ? null : (int) $value; - } -} diff --git a/config/auth.php b/config/auth.php index 7d1eb0de5..9e194cba5 100644 --- a/config/auth.php +++ b/config/auth.php @@ -62,7 +62,7 @@ return [ 'providers' => [ 'users' => [ 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', App\Models\User::class), + 'model' => env('AUTH_MODEL', Modules\User\App\Models\User::class), ], // 'users' => [ diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 584104c9c..a6e42427f 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -5,22 +5,14 @@ namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; +use Modules\User\App\Models\User; -/** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> - */ class UserFactory extends Factory { - /** - * The current password being used by the factory. - */ + protected $model = User::class; + protected static ?string $password; - /** - * Define the model's default state. - * - * @return array - */ public function definition(): array { return [ @@ -32,9 +24,6 @@ class UserFactory extends Factory ]; } - /** - * Indicate that the model's email address should be unverified. - */ public function unverified(): static { return $this->state(fn (array $attributes) => [ diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php deleted file mode 100644 index 05fb5d9ea..000000000 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ /dev/null @@ -1,49 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); - } -}; diff --git a/database/migrations/2026_03_03_130000_add_profile_and_status_to_users_table.php b/database/migrations/2026_03_03_130000_add_profile_and_status_to_users_table.php deleted file mode 100644 index c60e70b02..000000000 --- a/database/migrations/2026_03_03_130000_add_profile_and_status_to_users_table.php +++ /dev/null @@ -1,34 +0,0 @@ -string('avatar_url')->nullable()->after('password'); - } - - if (! Schema::hasColumn('users', 'status')) { - $table->string('status')->default('active')->after('email_verified_at'); - } - }); - } - - public function down(): void - { - Schema::table('users', function (Blueprint $table): void { - if (Schema::hasColumn('users', 'avatar_url')) { - $table->dropColumn('avatar_url'); - } - - if (Schema::hasColumn('users', 'status')) { - $table->dropColumn('status'); - } - }); - } -}; diff --git a/database/migrations/2026_03_03_190000_create_favorites_tables.php b/database/migrations/2026_03_03_190000_create_favorites_tables.php deleted file mode 100644 index 350194c75..000000000 --- a/database/migrations/2026_03_03_190000_create_favorites_tables.php +++ /dev/null @@ -1,49 +0,0 @@ -id(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - $table->foreignId('listing_id')->constrained('listings')->cascadeOnDelete(); - $table->timestamps(); - - $table->unique(['user_id', 'listing_id']); - }); - - Schema::create('favorite_sellers', function (Blueprint $table): void { - $table->id(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - $table->foreignId('seller_id')->constrained('users')->cascadeOnDelete(); - $table->timestamps(); - - $table->unique(['user_id', 'seller_id']); - }); - - Schema::create('favorite_searches', function (Blueprint $table): void { - $table->id(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - $table->string('label')->nullable(); - $table->string('search_term')->nullable(); - $table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete(); - $table->json('filters')->nullable(); - $table->string('signature', 64); - $table->timestamps(); - - $table->unique(['user_id', 'signature']); - }); - } - - public function down(): void - { - Schema::dropIfExists('favorite_searches'); - Schema::dropIfExists('favorite_sellers'); - Schema::dropIfExists('favorite_listings'); - } -}; diff --git a/database/migrations/2026_03_03_230000_create_conversations_tables.php b/database/migrations/2026_03_03_230000_create_conversations_tables.php deleted file mode 100644 index 49a13af3f..000000000 --- a/database/migrations/2026_03_03_230000_create_conversations_tables.php +++ /dev/null @@ -1,42 +0,0 @@ -id(); - $table->foreignId('listing_id')->constrained('listings')->cascadeOnDelete(); - $table->foreignId('seller_id')->constrained('users')->cascadeOnDelete(); - $table->foreignId('buyer_id')->constrained('users')->cascadeOnDelete(); - $table->timestamp('last_message_at')->nullable(); - $table->timestamps(); - - $table->unique(['listing_id', 'buyer_id']); - $table->index(['seller_id', 'last_message_at']); - $table->index(['buyer_id', 'last_message_at']); - }); - - Schema::create('conversation_messages', function (Blueprint $table): void { - $table->id(); - $table->foreignId('conversation_id')->constrained('conversations')->cascadeOnDelete(); - $table->foreignId('sender_id')->constrained('users')->cascadeOnDelete(); - $table->text('body'); - $table->timestamp('read_at')->nullable(); - $table->timestamps(); - - $table->index(['conversation_id', 'created_at']); - $table->index(['conversation_id', 'read_at']); - }); - } - - public function down(): void - { - Schema::dropIfExists('conversation_messages'); - Schema::dropIfExists('conversations'); - } -}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 3808b700c..f713d56d5 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -1,7 +1,7 @@ group(function () { Route::put('password', [PasswordController::class, 'update'])->name('password.update'); - Route::patch('profile', [\App\Http\Controllers\Auth\ProfileController::class, 'update'])->name('profile.update'); - Route::delete('profile', [\App\Http\Controllers\Auth\ProfileController::class, 'destroy'])->name('profile.destroy'); - Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) ->name('logout'); }); diff --git a/routes/web.php b/routes/web.php index 41003b77f..64af0c9f3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,5 @@ prefix('panel')->name('panel.')->group(function () { Route::get('/', [PanelController::class, 'index'])->name('index'); Route::get('/ilanlarim', [PanelController::class, 'listings'])->name('listings.index'); Route::get('/ilan-ver', [PanelController::class, 'create'])->name('listings.create'); - Route::get('/gelen-kutusu', [PanelController::class, 'inbox'])->name('inbox.index'); Route::post('/ilanlarim/{listing}/kaldir', [PanelController::class, 'destroyListing'])->name('listings.destroy'); Route::post('/ilanlarim/{listing}/satildi', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold'); Route::post('/ilanlarim/{listing}/yeniden-yayinla', [PanelController::class, 'republishListing'])->name('listings.republish'); @@ -27,17 +24,4 @@ Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () { Route::get('/partner/{any?}', fn () => redirect()->route('panel.listings.index')) ->where('any', '.*'); -Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () { - Route::get('/', [FavoriteController::class, 'index'])->name('index'); - Route::post('/listings/{listing}/toggle', [FavoriteController::class, 'toggleListing'])->name('listings.toggle'); - Route::post('/sellers/{seller}/toggle', [FavoriteController::class, 'toggleSeller'])->name('sellers.toggle'); - Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store'); - Route::delete('/searches/{favoriteSearch}', [FavoriteController::class, 'destroySearch'])->name('searches.destroy'); -}); - -Route::middleware('auth')->name('conversations.')->group(function () { - Route::post('/listings/{listing}/conversation', [ConversationController::class, 'start'])->name('start'); - Route::post('/conversations/{conversation}/messages', [ConversationController::class, 'send'])->name('messages.send'); -}); - require __DIR__.'/auth.php';