From 747e7410f4ac47551b816d38ced4f3556ec44439 Mon Sep 17 00:00:00 2001 From: fatihalp Date: Wed, 4 Mar 2026 00:29:08 +0300 Subject: [PATCH] Update partner listings flow --- ...33000_add_view_count_to_listings_table.php | 26 + .../Http/Controllers/ListingController.php | 19 +- Modules/Listing/Models/Listing.php | 3 +- .../Listing/resources/views/index.blade.php | 4 +- .../Listing/resources/views/show.blade.php | 4 +- .../Providers/PartnerServiceProvider.php | 5 +- .../Auth/PartnerAuthGatewayController.php | 27 - .../Controllers/ConversationController.php | 24 +- app/Http/Controllers/PanelController.php | 179 +++ app/Livewire/PanelQuickListingForm.php | 692 +++++++++ app/Models/User.php | 1 - app/Providers/AppServiceProvider.php | 18 - .../PartnerSocialRegistrationAvailability.php | 48 - bootstrap/providers.php | 1 - modules_statuses.json | 2 +- .../auth/registration-disabled.blade.php | 4 +- resources/views/errors/403.blade.php | 4 +- resources/views/favorites/index.blade.php | 14 +- .../partner/listings/quick-create.blade.php | 6 +- resources/views/home.blade.php | 8 +- resources/views/layouts/app.blade.php | 70 +- resources/views/panel/create.blade.php | 13 + resources/views/panel/inbox.blade.php | 163 +++ resources/views/panel/listings.blade.php | 143 ++ .../views/panel/partials/sidebar.blade.php | 28 + resources/views/panel/quick-create.blade.php | 1287 +++++++++++++++++ routes/auth.php | 8 +- routes/web.php | 31 +- 28 files changed, 2621 insertions(+), 211 deletions(-) create mode 100644 Modules/Listing/Database/migrations/2026_03_03_233000_add_view_count_to_listings_table.php delete mode 100644 app/Http/Controllers/Auth/PartnerAuthGatewayController.php create mode 100644 app/Http/Controllers/PanelController.php create mode 100644 app/Livewire/PanelQuickListingForm.php delete mode 100644 app/Support/PartnerSocialRegistrationAvailability.php create mode 100644 resources/views/panel/create.blade.php create mode 100644 resources/views/panel/inbox.blade.php create mode 100644 resources/views/panel/listings.blade.php create mode 100644 resources/views/panel/partials/sidebar.blade.php create mode 100644 resources/views/panel/quick-create.blade.php diff --git a/Modules/Listing/Database/migrations/2026_03_03_233000_add_view_count_to_listings_table.php b/Modules/Listing/Database/migrations/2026_03_03_233000_add_view_count_to_listings_table.php new file mode 100644 index 000000000..a20e3ec60 --- /dev/null +++ b/Modules/Listing/Database/migrations/2026_03_03_233000_add_view_count_to_listings_table.php @@ -0,0 +1,26 @@ +unsignedInteger('view_count')->default(0)->after('is_featured'); + } + }); + } + + public function down(): void + { + Schema::table('listings', function (Blueprint $table): void { + if (Schema::hasColumn('listings', 'view_count')) { + $table->dropColumn('view_count'); + } + }); + } +}; diff --git a/Modules/Listing/Http/Controllers/ListingController.php b/Modules/Listing/Http/Controllers/ListingController.php index b9e79b305..10ab84cd3 100644 --- a/Modules/Listing/Http/Controllers/ListingController.php +++ b/Modules/Listing/Http/Controllers/ListingController.php @@ -4,6 +4,7 @@ namespace Modules\Listing\Http\Controllers; use App\Http\Controllers\Controller; use App\Models\Conversation; use App\Models\FavoriteSearch; +use Illuminate\Support\Facades\Schema; use Modules\Category\Models\Category; use Modules\Listing\Models\Listing; use Modules\Listing\Support\ListingCustomFieldSchemaBuilder; @@ -82,6 +83,14 @@ class ListingController extends Controller public function show(Listing $listing) { + if ( + Schema::hasColumn('listings', 'view_count') + && (! auth()->check() || (int) auth()->id() !== (int) $listing->user_id) + ) { + $listing->increment('view_count'); + $listing->refresh(); + } + $listing->loadMissing('user:id,name,email'); $presentableCustomFields = ListingCustomFieldSchemaBuilder::presentableValues( $listing->category_id ? (int) $listing->category_id : null, @@ -127,20 +136,20 @@ class ListingController extends Controller public function create() { if (! auth()->check()) { - return redirect()->route('filament.partner.auth.login'); + return redirect()->route('login'); } - return redirect()->route('filament.partner.resources.listings.create', ['tenant' => auth()->id()]); + return redirect()->route('panel.listings.create'); } public function store() { if (! auth()->check()) { - return redirect()->route('filament.partner.auth.login'); + return redirect()->route('login'); } return redirect() - ->route('filament.partner.resources.listings.create', ['tenant' => auth()->id()]) - ->with('success', 'Use the Partner Panel to create listings.'); + ->route('panel.listings.create') + ->with('success', 'İlan oluşturma ekranına yönlendirildin.'); } } diff --git a/Modules/Listing/Models/Listing.php b/Modules/Listing/Models/Listing.php index c35181a73..58d90432f 100644 --- a/Modules/Listing/Models/Listing.php +++ b/Modules/Listing/Models/Listing.php @@ -22,13 +22,14 @@ class Listing extends Model implements HasMedia 'title', 'description', 'price', 'currency', 'category_id', 'user_id', 'status', 'images', 'custom_fields', 'slug', 'contact_phone', 'contact_email', 'is_featured', 'expires_at', - 'city', 'country', 'latitude', 'longitude', 'location', + 'city', 'country', 'latitude', 'longitude', 'location', 'view_count', ]; protected $casts = [ 'images' => 'array', 'custom_fields' => 'array', 'is_featured' => 'boolean', + 'view_count' => 'integer', 'expires_at' => 'datetime', 'price' => 'decimal:2', 'latitude' => 'decimal:7', diff --git a/Modules/Listing/resources/views/index.blade.php b/Modules/Listing/resources/views/index.blade.php index 1db745c9f..150ff189a 100644 --- a/Modules/Listing/resources/views/index.blade.php +++ b/Modules/Listing/resources/views/index.blade.php @@ -69,7 +69,7 @@ @else - + @endauth @@ -90,7 +90,7 @@ @auth @if($listing->user_id && (int) $listing->user_id !== (int) auth()->id()) @if($conversationId) - + Sohbete Git @else diff --git a/Modules/Listing/resources/views/show.blade.php b/Modules/Listing/resources/views/show.blade.php index ae10809eb..a219646e4 100644 --- a/Modules/Listing/resources/views/show.blade.php +++ b/Modules/Listing/resources/views/show.blade.php @@ -53,7 +53,7 @@ @if($existingConversationId) - + Sohbete Git @else @@ -66,7 +66,7 @@ @endif @endif @else - + Giriş yap ve favorile @endauth diff --git a/Modules/Partner/Providers/PartnerServiceProvider.php b/Modules/Partner/Providers/PartnerServiceProvider.php index 9fc991bbb..c17b5f41e 100644 --- a/Modules/Partner/Providers/PartnerServiceProvider.php +++ b/Modules/Partner/Providers/PartnerServiceProvider.php @@ -7,8 +7,5 @@ class PartnerServiceProvider extends ServiceProvider { public function boot(): void {} - public function register(): void - { - $this->app->register(PartnerPanelProvider::class); - } + public function register(): void {} } diff --git a/app/Http/Controllers/Auth/PartnerAuthGatewayController.php b/app/Http/Controllers/Auth/PartnerAuthGatewayController.php deleted file mode 100644 index ea6b69bde..000000000 --- a/app/Http/Controllers/Auth/PartnerAuthGatewayController.php +++ /dev/null @@ -1,27 +0,0 @@ -route('filament.partner.auth.login'); - } - - public function register(): RedirectResponse | Response - { - if (PartnerSocialRegistrationAvailability::isAvailable()) { - return redirect() - ->route('filament.partner.auth.login') - ->with('success', __('Registration is available via social login providers.')); - } - - return response()->view('auth.registration-disabled', status: Response::HTTP_FORBIDDEN); - } -} diff --git a/app/Http/Controllers/ConversationController.php b/app/Http/Controllers/ConversationController.php index d5c3f20c1..25ca9bb94 100644 --- a/app/Http/Controllers/ConversationController.php +++ b/app/Http/Controllers/ConversationController.php @@ -53,8 +53,8 @@ class ConversationController extends Controller } return redirect() - ->route('favorites.index', array_merge( - $this->listingTabFilters($request), + ->route('panel.inbox.index', array_merge( + $this->inboxFilters($request), ['conversation' => $conversation->getKey()], )) ->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.'); @@ -83,28 +83,16 @@ class ConversationController extends Controller ])->save(); return redirect() - ->route('favorites.index', array_merge( - $this->listingTabFilters($request), + ->route('panel.inbox.index', array_merge( + $this->inboxFilters($request), ['conversation' => $conversation->getKey()], )) ->with('success', 'Mesaj gönderildi.'); } - private function listingTabFilters(Request $request): array + private function inboxFilters(Request $request): array { - $filters = [ - 'tab' => 'listings', - ]; - - $status = (string) $request->string('status'); - if (in_array($status, ['all', 'active'], true)) { - $filters['status'] = $status; - } - - $categoryId = $request->integer('category'); - if ($categoryId > 0) { - $filters['category'] = $categoryId; - } + $filters = []; $messageFilter = (string) $request->string('message_filter'); if (in_array($messageFilter, ['all', 'unread', 'important'], true)) { diff --git a/app/Http/Controllers/PanelController.php b/app/Http/Controllers/PanelController.php new file mode 100644 index 000000000..618dc9175 --- /dev/null +++ b/app/Http/Controllers/PanelController.php @@ -0,0 +1,179 @@ +route('panel.listings.index'); + } + + public function create(): View + { + return view('panel.create'); + } + + public function listings(Request $request): View + { + $user = $request->user(); + $search = trim((string) $request->string('search')); + $status = (string) $request->string('status', 'all'); + + if (! in_array($status, ['all', 'sold', 'expired'], true)) { + $status = 'all'; + } + + $listings = $user->listings() + ->with('category:id,name') + ->withCount('favoritedByUsers') + ->when($search !== '', fn ($query) => $query->where('title', 'like', "%{$search}%")) + ->when($status !== 'all', fn ($query) => $query->where('status', $status)) + ->latest('id') + ->paginate(10) + ->withQueryString(); + + $statusCounts = $user->listings() + ->selectRaw('status, COUNT(*) as aggregate') + ->groupBy('status') + ->pluck('aggregate', 'status'); + + $counts = [ + 'all' => (int) $statusCounts->sum(), + 'sold' => (int) ($statusCounts['sold'] ?? 0), + 'expired' => (int) ($statusCounts['expired'] ?? 0), + ]; + + return view('panel.listings', [ + 'listings' => $listings, + 'status' => $status, + 'search' => $search, + 'counts' => $counts, + ]); + } + + 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:id,conversation_id,sender_id,body,created_at', + ]) + ->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); + $listing->delete(); + + return back()->with('success', 'İlan kaldırıldı.'); + } + + public function markListingAsSold(Request $request, Listing $listing): RedirectResponse + { + $this->guardListingOwner($request, $listing); + $listing->forceFill([ + 'status' => 'sold', + ])->save(); + + return back()->with('success', 'İlan satıldı olarak işaretlendi.'); + } + + public function republishListing(Request $request, Listing $listing): RedirectResponse + { + $this->guardListingOwner($request, $listing); + $listing->forceFill([ + 'status' => 'active', + 'expires_at' => now()->addDays(30), + ])->save(); + + return back()->with('success', 'İlan yeniden yayına alındı.'); + } + + private function guardListingOwner(Request $request, Listing $listing): void + { + if ((int) $listing->user_id !== (int) $request->user()->getKey()) { + abort(403); + } + } +} diff --git a/app/Livewire/PanelQuickListingForm.php b/app/Livewire/PanelQuickListingForm.php new file mode 100644 index 000000000..0f3484430 --- /dev/null +++ b/app/Livewire/PanelQuickListingForm.php @@ -0,0 +1,692 @@ +loadCategories(); + $this->loadLocations(); + $this->hydrateLocationDefaultsFromProfile(); + } + + public function render() + { + return view('panel.quick-create'); + } + + public function updatedPhotos(): void + { + $this->validatePhotos(); + } + + public function updatedSelectedCountryId(): void + { + $this->selectedCityId = null; + } + + public function removePhoto(int $index): void + { + if (! isset($this->photos[$index])) { + return; + } + + unset($this->photos[$index]); + $this->photos = array_values($this->photos); + } + + public function goToStep(int $step): void + { + $this->currentStep = max(1, min(self::TOTAL_STEPS, $step)); + } + + public function goToCategoryStep(): void + { + $this->validatePhotos(); + $this->currentStep = 2; + + if (! $this->isDetecting && ! $this->detectedCategoryId) { + $this->detectCategoryFromImage(); + } + } + + public function goToDetailsStep(): void + { + $this->validateCategoryStep(); + $this->currentStep = 3; + } + + public function goToFeaturesStep(): void + { + $this->validateCategoryStep(); + $this->validateDetailsStep(); + $this->currentStep = 4; + } + + public function goToPreviewStep(): void + { + $this->validateCategoryStep(); + $this->validateDetailsStep(); + $this->validateCustomFieldsStep(); + $this->currentStep = 5; + } + + public function detectCategoryFromImage(): void + { + if ($this->photos === []) { + return; + } + + $this->isDetecting = true; + $this->detectedError = null; + $this->detectedReason = null; + $this->detectedAlternatives = []; + + $result = app(QuickListingCategorySuggester::class)->suggestFromImage($this->photos[0]); + + $this->isDetecting = false; + $this->detectedCategoryId = $result['category_id']; + $this->detectedConfidence = $result['confidence']; + $this->detectedReason = $result['reason']; + $this->detectedError = $result['error']; + $this->detectedAlternatives = $result['alternatives']; + + if ($this->detectedCategoryId) { + $this->selectCategory($this->detectedCategoryId); + } + } + + public function enterCategory(int $categoryId): void + { + if (! $this->categoryExists($categoryId)) { + return; + } + + $this->activeParentCategoryId = $categoryId; + $this->categorySearch = ''; + } + + public function backToRootCategories(): void + { + $this->activeParentCategoryId = null; + $this->categorySearch = ''; + } + + public function selectCategory(int $categoryId): void + { + if (! $this->categoryExists($categoryId)) { + return; + } + + $this->selectedCategoryId = $categoryId; + $this->loadListingCustomFields(); + } + + public function publishListing(): ?RedirectResponse + { + if ($this->isPublishing) { + return null; + } + + $this->isPublishing = true; + + $this->validatePhotos(); + $this->validateCategoryStep(); + $this->validateDetailsStep(); + $this->validateCustomFieldsStep(); + + try { + $this->createListing(); + } catch (Throwable $exception) { + report($exception); + $this->isPublishing = false; + session()->flash('error', 'İlan oluşturulamadı. Lütfen tekrar deneyin.'); + + return null; + } + + $this->isPublishing = false; + session()->flash('success', 'İlan başarıyla oluşturuldu.'); + + return redirect()->route('panel.listings.index'); + } + + public function getRootCategoriesProperty(): array + { + return collect($this->categories) + ->whereNull('parent_id') + ->values() + ->all(); + } + + public function getCurrentCategoriesProperty(): array + { + if (! $this->activeParentCategoryId) { + return []; + } + + $search = trim((string) $this->categorySearch); + $all = collect($this->categories); + $parent = $all->firstWhere('id', $this->activeParentCategoryId); + $children = $all->where('parent_id', $this->activeParentCategoryId)->values(); + + $combined = collect(); + + if (is_array($parent)) { + $combined->push($parent); + } + + $combined = $combined->concat($children); + + return $combined + ->when( + $search !== '', + fn (Collection $categories): Collection => $categories->filter( + fn (array $category): bool => str_contains( + mb_strtolower($category['name']), + mb_strtolower($search) + ) + ) + ) + ->values() + ->all(); + } + + public function getCurrentParentNameProperty(): string + { + if (! $this->activeParentCategoryId) { + return 'Kategori Seçimi'; + } + + $category = collect($this->categories)->firstWhere('id', $this->activeParentCategoryId); + + return (string) ($category['name'] ?? 'Kategori Seçimi'); + } + + public function getCurrentStepTitleProperty(): string + { + return match ($this->currentStep) { + 1 => 'Fotoğraf', + 2 => 'Kategori Seçimi', + 3 => 'İlan Bilgileri', + 4 => 'İlan Özellikleri', + 5 => 'İlan Önizlemesi', + default => 'İlan Ver', + }; + } + + public function getSelectedCategoryNameProperty(): ?string + { + if (! $this->selectedCategoryId) { + return null; + } + + $category = collect($this->categories)->firstWhere('id', $this->selectedCategoryId); + + return $category['name'] ?? null; + } + + public function getSelectedCategoryPathProperty(): string + { + if (! $this->selectedCategoryId) { + return ''; + } + + return implode(' › ', $this->categoryPathParts($this->selectedCategoryId)); + } + + public function getDetectedAlternativeNamesProperty(): array + { + if ($this->detectedAlternatives === []) { + return []; + } + + $categoriesById = collect($this->categories)->keyBy('id'); + + return collect($this->detectedAlternatives) + ->map(fn (int $id): ?string => $categoriesById[$id]['name'] ?? null) + ->filter() + ->values() + ->all(); + } + + public function getAvailableCitiesProperty(): array + { + if (! $this->selectedCountryId) { + return []; + } + + return collect($this->cities) + ->where('country_id', $this->selectedCountryId) + ->values() + ->all(); + } + + public function getSelectedCountryNameProperty(): ?string + { + if (! $this->selectedCountryId) { + return null; + } + + $country = collect($this->countries)->firstWhere('id', $this->selectedCountryId); + + return $country['name'] ?? null; + } + + public function getSelectedCityNameProperty(): ?string + { + if (! $this->selectedCityId) { + return null; + } + + $city = collect($this->cities)->firstWhere('id', $this->selectedCityId); + + return $city['name'] ?? null; + } + + public function getPreviewCustomFieldsProperty(): array + { + return ListingCustomFieldSchemaBuilder::presentableValues( + $this->selectedCategoryId, + $this->sanitizedCustomFieldValues(), + ); + } + + public function getTitleCharactersProperty(): int + { + return mb_strlen($this->listingTitle); + } + + public function getDescriptionCharactersProperty(): int + { + return mb_strlen($this->description); + } + + public function getCurrentUserNameProperty(): string + { + return (string) (auth()->user()?->name ?: 'Kullanıcı'); + } + + public function getCurrentUserInitialProperty(): string + { + return Str::upper(Str::substr($this->currentUserName, 0, 1)); + } + + public function categoryIconComponent(?string $icon): string + { + return match ($icon) { + 'car' => 'heroicon-o-truck', + 'laptop', 'computer' => 'heroicon-o-computer-desktop', + 'shirt' => 'heroicon-o-swatch', + 'home', 'sofa' => 'heroicon-o-home-modern', + 'briefcase' => 'heroicon-o-briefcase', + 'wrench' => 'heroicon-o-wrench-screwdriver', + 'football' => 'heroicon-o-trophy', + 'phone', 'mobile' => 'heroicon-o-device-phone-mobile', + default => 'heroicon-o-tag', + }; + } + + private function validatePhotos(): void + { + $this->validate([ + 'photos' => [ + 'required', + 'array', + 'min:1', + 'max:'.config('quick-listing.max_photo_count', 20), + ], + 'photos.*' => [ + 'required', + 'image', + 'mimes:jpg,jpeg,png', + 'max:'.config('quick-listing.max_photo_size_kb', 5120), + ], + ]); + } + + private function validateCategoryStep(): void + { + $this->validate([ + 'selectedCategoryId' => [ + 'required', + 'integer', + Rule::in(collect($this->categories)->pluck('id')->all()), + ], + ], [ + 'selectedCategoryId.required' => 'Lütfen bir kategori seçin.', + 'selectedCategoryId.in' => 'Geçerli bir kategori seçin.', + ]); + } + + private function validateDetailsStep(): void + { + $this->validate([ + 'listingTitle' => ['required', 'string', 'max:70'], + 'price' => ['required', 'numeric', 'min:0'], + 'description' => ['required', 'string', 'max:1450'], + 'selectedCountryId' => ['required', 'integer', Rule::in(collect($this->countries)->pluck('id')->all())], + 'selectedCityId' => [ + 'required', + 'integer', + function (string $attribute, mixed $value, \Closure $fail): void { + $cityExists = collect($this->availableCities) + ->contains(fn (array $city): bool => $city['id'] === (int) $value); + + if (! $cityExists) { + $fail('Seçtiğiniz şehir, seçilen ülkeye ait değil.'); + } + }, + ], + ], [ + 'listingTitle.required' => 'İlan başlığı zorunludur.', + 'listingTitle.max' => 'İlan başlığı en fazla 70 karakter olabilir.', + 'price.required' => 'Fiyat zorunludur.', + 'price.numeric' => 'Fiyat sayısal olmalıdır.', + 'description.required' => 'Açıklama zorunludur.', + 'description.max' => 'Açıklama en fazla 1450 karakter olabilir.', + 'selectedCountryId.required' => 'Ülke seçimi zorunludur.', + 'selectedCityId.required' => 'Şehir seçimi zorunludur.', + ]); + } + + private function validateCustomFieldsStep(): void + { + $rules = []; + + foreach ($this->listingCustomFields as $field) { + $fieldRules = []; + $name = $field['name']; + $statePath = "customFieldValues.{$name}"; + $type = $field['type']; + $isRequired = (bool) $field['is_required']; + + if ($type === ListingCustomField::TYPE_BOOLEAN) { + $fieldRules[] = 'nullable'; + $fieldRules[] = 'boolean'; + } else { + $fieldRules[] = $isRequired ? 'required' : 'nullable'; + } + + $fieldRules[] = match ($type) { + ListingCustomField::TYPE_TEXT => 'string|max:255', + ListingCustomField::TYPE_TEXTAREA => 'string|max:2000', + ListingCustomField::TYPE_NUMBER => 'numeric', + ListingCustomField::TYPE_DATE => 'date', + default => 'sometimes', + }; + + if ($type === ListingCustomField::TYPE_SELECT) { + $options = collect($field['options'] ?? [])->map(fn ($option): string => (string) $option)->all(); + $fieldRules[] = Rule::in($options); + } + + $rules[$statePath] = $fieldRules; + } + + if ($rules !== []) { + $this->validate($rules); + } + } + + private function createListing(): Listing + { + $user = auth()->user(); + + if (! $user) { + abort(403); + } + + $profilePhone = Profile::query() + ->where('user_id', $user->getKey()) + ->value('phone'); + + $payload = [ + 'title' => trim($this->listingTitle), + 'description' => trim($this->description), + 'price' => (float) $this->price, + 'currency' => ListingPanelHelper::defaultCurrency(), + 'category_id' => $this->selectedCategoryId, + 'status' => 'pending', + 'custom_fields' => $this->sanitizedCustomFieldValues(), + 'contact_email' => (string) $user->email, + 'contact_phone' => $profilePhone, + 'country' => $this->selectedCountryName, + 'city' => $this->selectedCityName, + ]; + + $listing = Listing::createFromFrontend($payload, $user->getKey()); + + foreach ($this->photos as $photo) { + if (! $photo instanceof TemporaryUploadedFile) { + continue; + } + + $listing + ->addMedia($photo->getRealPath()) + ->usingFileName($photo->getClientOriginalName()) + ->toMediaCollection('listing-images'); + } + + return $listing; + } + + private function sanitizedCustomFieldValues(): array + { + $fieldsByName = collect($this->listingCustomFields)->keyBy('name'); + + return collect($this->customFieldValues) + ->filter(fn ($value, $key): bool => $fieldsByName->has((string) $key)) + ->map(function ($value, $key) use ($fieldsByName): mixed { + $field = $fieldsByName->get((string) $key); + $type = (string) ($field['type'] ?? ListingCustomField::TYPE_TEXT); + + return match ($type) { + ListingCustomField::TYPE_NUMBER => is_numeric($value) ? (float) $value : null, + ListingCustomField::TYPE_BOOLEAN => (bool) $value, + default => is_string($value) ? trim($value) : $value, + }; + }) + ->filter(function ($value, $key) use ($fieldsByName): bool { + $field = $fieldsByName->get((string) $key); + $type = (string) ($field['type'] ?? ListingCustomField::TYPE_TEXT); + + if ($type === ListingCustomField::TYPE_BOOLEAN) { + return true; + } + + return ! is_null($value) && $value !== ''; + }) + ->all(); + } + + private function loadCategories(): void + { + $all = Category::query() + ->where('is_active', true) + ->orderBy('sort_order') + ->orderBy('name') + ->get(['id', 'name', 'parent_id', 'icon']); + + $childrenCount = Category::query() + ->where('is_active', true) + ->selectRaw('parent_id, count(*) as aggregate') + ->whereNotNull('parent_id') + ->groupBy('parent_id') + ->pluck('aggregate', 'parent_id'); + + $this->categories = $all + ->map(fn (Category $category): array => [ + 'id' => (int) $category->id, + 'name' => (string) $category->name, + 'parent_id' => $category->parent_id ? (int) $category->parent_id : null, + 'icon' => $category->icon, + 'has_children' => ((int) ($childrenCount[$category->id] ?? 0)) > 0, + ]) + ->values() + ->all(); + } + + private function loadLocations(): void + { + $this->countries = Country::query() + ->where('is_active', true) + ->orderBy('name') + ->get(['id', 'name']) + ->map(fn (Country $country): array => [ + 'id' => (int) $country->id, + 'name' => (string) $country->name, + ]) + ->all(); + + $this->cities = City::query() + ->where('is_active', true) + ->orderBy('name') + ->get(['id', 'name', 'country_id']) + ->map(fn (City $city): array => [ + 'id' => (int) $city->id, + 'name' => (string) $city->name, + 'country_id' => (int) $city->country_id, + ]) + ->all(); + } + + private function loadListingCustomFields(): void + { + $this->listingCustomFields = ListingCustomField::query() + ->active() + ->forCategory($this->selectedCategoryId) + ->ordered() + ->get(['name', 'label', 'type', 'is_required', 'placeholder', 'help_text', 'options']) + ->map(fn (ListingCustomField $field): array => [ + 'name' => (string) $field->name, + 'label' => (string) $field->label, + 'type' => (string) $field->type, + 'is_required' => (bool) $field->is_required, + 'placeholder' => $field->placeholder, + 'help_text' => $field->help_text, + 'options' => collect($field->options ?? []) + ->map(fn ($option): string => (string) $option) + ->values() + ->all(), + ]) + ->all(); + + $allowed = collect($this->listingCustomFields)->pluck('name')->all(); + $this->customFieldValues = collect($this->customFieldValues)->only($allowed)->all(); + + foreach ($this->listingCustomFields as $field) { + if ($field['type'] === ListingCustomField::TYPE_BOOLEAN && ! array_key_exists($field['name'], $this->customFieldValues)) { + $this->customFieldValues[$field['name']] = false; + } + } + } + + private function hydrateLocationDefaultsFromProfile(): void + { + $user = auth()->user(); + + if (! $user) { + return; + } + + $profile = Profile::query()->where('user_id', $user->getKey())->first(); + + if (! $profile) { + return; + } + + $profileCountry = trim((string) ($profile->country ?? '')); + $profileCity = trim((string) ($profile->city ?? '')); + + if ($profileCountry !== '') { + $country = collect($this->countries)->first(fn (array $country): bool => mb_strtolower($country['name']) === mb_strtolower($profileCountry)); + + if (is_array($country)) { + $this->selectedCountryId = $country['id']; + } + } + + if ($profileCity !== '' && $this->selectedCountryId) { + $city = collect($this->availableCities)->first(fn (array $city): bool => mb_strtolower($city['name']) === mb_strtolower($profileCity)); + + if (is_array($city)) { + $this->selectedCityId = $city['id']; + } + } + } + + private function categoryExists(int $categoryId): bool + { + return collect($this->categories)->contains(fn (array $category): bool => $category['id'] === $categoryId); + } + + private function categoryPathParts(int $categoryId): array + { + $byId = collect($this->categories)->keyBy('id'); + $parts = []; + $currentId = $categoryId; + + while ($currentId && $byId->has($currentId)) { + $category = $byId->get($currentId); + + if (! is_array($category)) { + break; + } + + $parts[] = (string) $category['name']; + $currentId = $category['parent_id'] ?? null; + } + + return array_reverse($parts); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index f7c48d3f3..2c97ad261 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -48,7 +48,6 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata { return match ($panel->getId()) { 'admin' => $this->hasRole('admin'), - 'partner' => true, default => false, }; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c2bcdc67d..dd4f199c9 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -8,7 +8,6 @@ use BezhanSalleh\LanguageSwitch\LanguageSwitch; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Gate; -use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\View; @@ -33,25 +32,8 @@ class AppServiceProvider extends ServiceProvider return null; }); - Route::pattern('tenant', '[0-9]+'); View::addNamespace('app', resource_path('views')); - app()->booted(function (): void { - foreach (app('router')->getRoutes() as $route) { - $name = $route->getName(); - - if (! is_string($name) || ! str_starts_with($name, 'filament.partner.')) { - continue; - } - - if (! str_contains($route->uri(), '{tenant}')) { - continue; - } - - $route->where('tenant', '[0-9]+'); - } - }); - $fallbackName = config('app.name', 'OpenClassify'); $fallbackLocale = config('app.locale', 'en'); $fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD'])); diff --git a/app/Support/PartnerSocialRegistrationAvailability.php b/app/Support/PartnerSocialRegistrationAvailability.php deleted file mode 100644 index 32bd51bd1..000000000 --- a/app/Support/PartnerSocialRegistrationAvailability.php +++ /dev/null @@ -1,48 +0,0 @@ - - */ - private const PROVIDERS = ['google', 'facebook', 'apple']; - - public static function isAvailable(): bool - { - foreach (self::PROVIDERS as $provider) { - if (self::providerEnabled($provider) && self::providerCredentialsReady($provider)) { - return true; - } - } - - return false; - } - - private static function providerEnabled(string $provider): bool - { - try { - /** @var GeneralSettings $settings */ - $settings = app(GeneralSettings::class); - - return match ($provider) { - 'google' => (bool) ($settings->enable_google_login ?? false), - 'facebook' => (bool) ($settings->enable_facebook_login ?? false), - 'apple' => (bool) ($settings->enable_apple_login ?? false), - default => false, - }; - } catch (Throwable) { - return (bool) config("services.{$provider}.enabled", false); - } - } - - private static function providerCredentialsReady(string $provider): bool - { - return filled(config("services.{$provider}.client_id")) - && filled(config("services.{$provider}.client_secret")); - } -} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 63cd44717..20543459c 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,5 +3,4 @@ return [ App\Providers\AppServiceProvider::class, Modules\Admin\Providers\AdminPanelProvider::class, - Modules\Partner\Providers\PartnerPanelProvider::class, ]; diff --git a/modules_statuses.json b/modules_statuses.json index dfefb0339..35aa86625 100644 --- a/modules_statuses.json +++ b/modules_statuses.json @@ -4,5 +4,5 @@ "Location": true, "Profile": true, "Admin": true, - "Partner": true + "Partner": false } diff --git a/resources/views/auth/registration-disabled.blade.php b/resources/views/auth/registration-disabled.blade.php index 77185cc9d..f3e0bd213 100644 --- a/resources/views/auth/registration-disabled.blade.php +++ b/resources/views/auth/registration-disabled.blade.php @@ -14,8 +14,8 @@ Back Home - - Partner Login + + Giriş Yap diff --git a/resources/views/errors/403.blade.php b/resources/views/errors/403.blade.php index ae0fcb43b..5a47fa522 100644 --- a/resources/views/errors/403.blade.php +++ b/resources/views/errors/403.blade.php @@ -18,14 +18,14 @@ @auth -
+ @csrf
@else - + Giriş Yap @endauth diff --git a/resources/views/favorites/index.blade.php b/resources/views/favorites/index.blade.php index aea427044..4e1b80fc0 100644 --- a/resources/views/favorites/index.blade.php +++ b/resources/views/favorites/index.blade.php @@ -5,17 +5,7 @@ @section('content')
- + @include('panel.partials.sidebar', ['activeMenu' => 'favorites', 'activeFavoritesTab' => $activeTab])
@if($activeTab === 'listings') @@ -98,7 +88,7 @@ @if($canMessageListing) @if($conversationId) - + Sohbete Git @else diff --git a/resources/views/filament/partner/listings/quick-create.blade.php b/resources/views/filament/partner/listings/quick-create.blade.php index 0cdb519b6..00ace84d0 100644 --- a/resources/views/filament/partner/listings/quick-create.blade.php +++ b/resources/views/filament/partner/listings/quick-create.blade.php @@ -1,4 +1,4 @@ - +
+ @livewireStyles
- @if($isHomePage && $homeHeaderCategories->isNotEmpty()) + @if(!$isSimplePage && $isHomePage && $homeHeaderCategories->isNotEmpty()) - @elseif(! $isHomePage) + @elseif(! $isSimplePage && ! $isHomePage) + @livewireScripts