'$refresh', ]; protected $queryString = [ 'search' => ['except' => ''], 'statusFilter' => ['except' => ''], 'callableFilter' => ['except' => ''], 'localeFilter' => ['except' => ''], 'perPage' => ['except' => 10], 'sortField' => ['except' => 'id'], 'sortDirection' => ['except' => 'desc'], ]; public function mount(): void { $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); if (!$activeProfileType || !$activeProfileId) { abort(403); } $profile = $activeProfileType::find($activeProfileId); if (!$profile) { abort(403); } ProfileAuthorizationHelper::authorize($profile); // Admin and Central Bank get full view of all calls if ($profile instanceof \App\Models\Admin) { $this->isAdminView = true; } elseif ($profile instanceof \App\Models\Bank && $profile->level === 0) { $this->isAdminView = true; } elseif ($profile instanceof \App\Models\User || $profile instanceof \App\Models\Organization || $profile instanceof \App\Models\Bank) { $this->isAdminView = false; } else { abort(403); } } // Listeners for location dropdown child component public function countryToParent($value): void { $this->editCountry = $value ?: null; } public function divisionToParent($value): void { $this->editDivision = $value ?: null; } public function cityToParent($value): void { $this->editCity = $value ?: null; } public function districtToParent($value): void { $this->editDistrict = $value ?: null; } public function callTagSelected(int $tagId): void { $this->editTagId = $tagId; $this->resetValidation('editTagId'); } public function callTagCleared(): void { $this->editTagId = null; } public function updatedShowEditModal(bool $value): void { if (!$value) { $this->dispatch('edit-done'); } } public function openEdit(int $id): void { $call = $this->findCall($id); $this->editCallId = $call->id; $this->editTagId = $call->tag_id; $this->editContent = $call->translations->firstWhere('locale', App::getLocale())?->content ?? $call->translations->first()?->content ?? ''; $this->editTill = $call->till?->format('Y-m-d'); $this->editIsPublic = (bool) $call->is_public; $location = $call->location; $this->editCountry = $location?->country_id; $this->editDivision = $location?->division_id; $this->editCity = $location?->city_id; $this->editDistrict = $location?->district_id; $this->resetValidation(); $this->showEditModal = true; } protected function editRules(): array { $call = $this->editCallId ? Call::find($this->editCallId) : null; $callableType = $call?->callable_type ?? session('activeProfileType'); $tillMaxDays = ($callableType && $callableType !== \App\Models\User::class) ? timebank_config('calls.till_max_days_non_user') : timebank_config('calls.till_max_days'); $tillRule = 'required|date|after:today'; if ($tillMaxDays !== null) { $tillRule .= '|before_or_equal:' . now()->addDays($tillMaxDays)->format('Y-m-d'); } return [ 'editContent' => ['required', 'string', 'max:' . timebank_config('calls.content_max_input', 200)], 'editTill' => $tillRule, 'editTagId' => 'required|integer|exists:taggable_tags,tag_id', 'editCountry' => 'required|integer|exists:countries,id', 'editCity' => 'nullable|integer|exists:cities,id', 'editDistrict'=> 'nullable|integer|exists:districts,id', ]; } public function saveEdit(): void { $this->validate($this->editRules()); $call = $this->findCall($this->editCallId); $locationId = null; if ($this->editCountry || $this->editCity) { $attributes = array_filter([ 'country_id' => $this->editCountry ?: null, 'division_id' => $this->editDivision ?: null, 'city_id' => $this->editCity ?: null, 'district_id' => $this->editDistrict ?: null, ]); $location = Location::whereNull('locatable_id') ->whereNull('locatable_type') ->where($attributes) ->first(); if (!$location) { $location = new Location($attributes); $location->save(); } $locationId = $location->id; } $call->update([ 'tag_id' => $this->editTagId, 'location_id' => $locationId, 'till' => $this->editTill ?: null, 'is_public' => $this->editIsPublic, ]); CallTranslation::updateOrCreate( ['call_id' => $call->id, 'locale' => App::getLocale()], ['content' => $this->editContent ?: null] ); $call->searchable(); $this->showEditModal = false; $this->editCallId = null; $this->dispatch('edit-done'); $this->notification()->success(title: __('Saved'), description: __('Your Call has been updated.')); } public function confirmDelete(): void { $this->showDeleteConfirm = true; } public function deleteCall(): void { $call = $this->findCall($this->editCallId); $call->unsearchable(); $call->translations()->delete(); $call->delete(); $this->showEditModal = false; $this->showDeleteConfirm = false; $this->editCallId = null; $this->notification()->success(title: __('Deleted'), description: __('The call has been deleted.')); } public function sortBy(string $field): void { if ($this->sortField === $field) { $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; } else { $this->sortField = $field; $this->sortDirection = 'desc'; } $this->resetPage(); } public function updatedSearch(): void { $this->resetPage(); } public function updatedStatusFilter(): void { $this->resetPage(); $this->bulkSelected = []; } public function updatedCallableFilter(): void { $this->resetPage(); $this->bulkSelected = []; } public function updatedLocaleFilter(): void { $this->resetPage(); $this->bulkSelected = []; } public function updatedPerPage(): void { $this->resetPage(); } public function updatedSelectAll(bool $value): void { if ($value) { $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); $query = Call::query(); if (!$this->isAdminView) { $query->where('callable_type', $activeProfileType)->where('callable_id', $activeProfileId); } if ($this->statusFilter === 'deleted') { $query->onlyTrashed(); } $this->bulkSelected = $query->pluck('id')->map(fn ($id) => (string) $id)->toArray(); } else { $this->bulkSelected = []; } } public function confirmBulkDelete(): void { $this->showDeleteConfirmModal = true; } public function confirmAdminDelete(): void { $calls = Call::whereIn('id', $this->bulkSelected) ->with('callable') ->get(); $nameList = $calls->map(fn ($c) => $c->callable?->name ?? '?')->unique()->values()->all(); $names = count($nameList) > 1 ? implode(', ', array_slice($nameList, 0, -1)) . ' ' . __('and') . ' ' . end($nameList) : ($nameList[0] ?? '?'); $this->adminDeleteCallableNames = $names; $this->showAdminDeleteConfirm = true; } public function deleteSelected(): void { $this->authorizeWrite(); $calls = Call::whereIn('id', $this->bulkSelected)->get(); foreach ($calls as $call) { $call->unsearchable(); $call->translations()->delete(); $call->delete(); } $this->bulkSelected = []; $this->selectAll = false; $this->showAdminDeleteConfirm = false; $this->adminDeleteCallableNames = ''; $this->showDeleteConfirmModal = false; $this->notification()->success(title: __('Deleted'), description: __('Selected calls have been deleted.')); } public function undeleteSelected(): void { $this->authorizeWrite(); $calls = Call::onlyTrashed()->whereIn('id', $this->bulkSelected)->get(); foreach ($calls as $call) { $call->translations()->withTrashed()->restore(); $call->restore(); $call->searchable(); } $this->bulkSelected = []; $this->selectAll = false; $this->notification()->success(title: __('Restored'), description: __('Selected calls have been restored.')); } public function confirmAdminAction(int $id, string $type): void { $call = $this->findCall($id); $this->adminActionCallId = $id; $this->adminActionType = $type; $this->adminActionCallableName = $call->callable?->name ?? '?'; $this->showAdminActionConfirm = true; $this->dispatch('admin-action-ready'); } public function executeAdminAction(): void { if (!$this->adminActionCallId || !$this->adminActionType) { return; } if ($this->adminActionType === 'pause') { $this->adminPause($this->adminActionCallId); } elseif ($this->adminActionType === 'publish') { $this->adminPublish($this->adminActionCallId); } $this->showAdminActionConfirm = false; $this->adminActionCallId = null; $this->adminActionType = ''; $this->adminActionCallableName = ''; } public function adminPause(int $id): void { if (!$this->isAdminView) abort(403); $call = Call::findOrFail($id); $call->update(['is_paused' => true]); $call->unsearchable(); $this->dispatch('pause-publish-done'); $this->notification()->success(title: __('Paused'), description: __('The call has been paused.')); } public function adminPublish(int $id): void { if (!$this->isAdminView) abort(403); $call = Call::findOrFail($id); $call->update(['is_paused' => false]); if ($call->shouldBeSearchable()) { $call->searchable(); } $this->dispatch('pause-publish-done'); $this->notification()->success(title: __('Published'), description: __('The call has been published.')); } public function pause(int $id): void { $this->authorizeWrite(); $call = $this->findCall($id); $call->update(['is_paused' => true]); $call->unsearchable(); $this->dispatch('pause-publish-done'); $this->notification()->success(title: __('Paused'), description: __('The call has been paused.')); } public function publish(int $id): void { $this->authorizeWrite(); $call = $this->findCall($id); if (!CallCreditService::profileHasCredits($call->callable_type, $call->callable_id)) { $this->notification()->error( title: __('Cannot publish'), description: trans_with_platform(__('You need @PLATFORM_CURRENCY_NAME_PLURAL@ to post a call.')) ); $this->dispatch('pause-publish-done'); return; } // If till has expired, reset it to the max allowed duration $updates = ['is_paused' => false]; if ($call->till === null || $call->till->isPast()) { $updates['till'] = now()->addDays(timebank_config('calls.till_max_days', 90)); } $call->update($updates); $call->searchable(); $this->dispatch('pause-publish-done'); $this->notification()->success(title: __('Published'), description: __('The call has been published.')); } public function block(int $id): void { if (!$this->isAdminView) { abort(403); } $call = Call::findOrFail($id); $call->update(['is_suppressed' => true]); $call->unsearchable(); $callable = $call->callable; if ($callable && $callable->email) { Mail::to($callable->email)->queue( new CallBlockedMail( $call->load(['tag']), $callable, class_basename($callable) ) ); } $this->notification()->error(title: __('Blocked'), description: __('The call has been blocked.')); } public function unblock(int $id): void { if (!$this->isAdminView) { abort(403); } $call = Call::findOrFail($id); $call->update(['is_suppressed' => false]); if ($call->shouldBeSearchable()) { $call->searchable(); } $this->notification()->success(title: __('Unblocked'), description: __('The call has been unblocked.')); } private function findCall(int $id): Call { if ($this->isAdminView) { return Call::findOrFail($id); } return Call::where('callable_type', session('activeProfileType')) ->where('callable_id', session('activeProfileId')) ->findOrFail($id); } private function authorizeWrite(): void { $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); if (!$activeProfileType || !$activeProfileId) { abort(403); } $profile = $activeProfileType::find($activeProfileId); ProfileAuthorizationHelper::authorize($profile); } public function render() { $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); $query = Call::with([ 'callable', 'tag.contexts.category', 'location.city.translations', 'location.country.translations', 'translations', ]); // Scope to own calls for non-admin if (!$this->isAdminView) { $query->where('callable_type', $activeProfileType) ->where('callable_id', $activeProfileId); } // Callable type filter (admin view) if ($this->isAdminView && $this->callableFilter) { $map = [ 'user' => \App\Models\User::class, 'organization' => \App\Models\Organization::class, 'bank' => \App\Models\Bank::class, ]; if (isset($map[$this->callableFilter])) { $query->where('callable_type', $map[$this->callableFilter]); } } // Status / deleted if ($this->statusFilter === 'deleted') { $query->onlyTrashed(); } elseif ($this->statusFilter === 'active') { $query->where('is_paused', false) ->where(fn ($q) => $q->whereNull('till')->orWhere('till', '>=', now())); } elseif ($this->statusFilter === 'expired') { $query->where('till', '<', now()); } elseif ($this->statusFilter === 'paused') { $query->where('is_paused', true); } // Search if ($this->search) { $search = '%' . $this->search . '%'; $query->where(function ($q) use ($search) { $q->whereHas('translations', fn ($t) => $t->where('content', 'like', $search)) ->orWhereHas('tag', fn ($t) => $t->where('name', 'like', $search)) ->orWhereHas('callable', fn ($t) => $t->where('name', 'like', $search)); }); } // Locale filter if ($this->localeFilter) { $query->whereHas('translations', fn ($q) => $q->where('locale', $this->localeFilter)); } $allowedSorts = ['id', 'created_at', 'till', 'updated_at']; if (in_array($this->sortField, $allowedSorts)) { $query->orderBy($this->sortField, $this->sortDirection); } else { // Sort by first locale via subquery to avoid duplicate rows $localeSubquery = DB::table('call_translations') ->select('call_id', DB::raw('MIN(locale) as first_locale')) ->whereNull('deleted_at') ->groupBy('call_id'); $query->leftJoinSub($localeSubquery, 'ct_locale', 'ct_locale.call_id', '=', 'calls.id') ->select('calls.*') ->orderBy('ct_locale.first_locale', $this->sortDirection) ->orderBy('calls.created_at', 'desc'); } $calls = $query->paginate($this->perPage); $editCall = $this->editCallId ? Call::find($this->editCallId) : null; // For non-admin view, check if the active profile has credits $canPublish = true; if (!$this->isAdminView && $activeProfileType && $activeProfileId) { $canPublish = CallCreditService::profileHasCredits($activeProfileType, (int) $activeProfileId); } $availableLocales = \App\Models\CallTranslation::select('locale') ->distinct() ->orderBy('locale') ->pluck('locale') ->map(fn ($locale) => ['id' => $locale, 'name' => strtoupper($locale)]) ->values() ->all(); return view('livewire.calls.manage', [ 'calls' => $calls, 'bulkDisabled' => empty($this->bulkSelected), 'editCall' => $editCall, 'canPublish' => $canPublish, 'availableLocales' => $availableLocales, ]); } }