Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,800 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Calls;
|
||||
|
||||
use App\Helpers\ProfileAuthorizationHelper;
|
||||
use App\Http\Livewire\Traits\RequiresAdminAuthorization;
|
||||
use App\Mail\CallBlockedMail;
|
||||
use App\Models\Call;
|
||||
use App\Models\CallTranslation;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\Tag;
|
||||
use App\Services\CallCreditService;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
class Manage extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
use WireUiActions;
|
||||
use RequiresAdminAuthorization;
|
||||
|
||||
public string $search = '';
|
||||
public ?string $statusFilter = ''; // 'active', 'expired', 'deleted'
|
||||
public ?string $callableFilter = ''; // 'user', 'organization', 'bank'
|
||||
public ?string $localeFilter = ''; // 'en', 'nl', 'de', etc.
|
||||
|
||||
public array $bulkSelected = [];
|
||||
public bool $selectAll = false;
|
||||
public int $perPage = 10;
|
||||
public string $sortField = 'id';
|
||||
public string $sortDirection = 'desc';
|
||||
|
||||
// Locked: must not be client-settable — escalating this to true would grant admin privileges
|
||||
#[Locked] public bool $isAdminView = false;
|
||||
|
||||
// Inline edit state
|
||||
public ?int $editCallId = null;
|
||||
public string $editContent = '';
|
||||
public ?string $editTill = null;
|
||||
public ?int $editTagId = null;
|
||||
public bool $editIsPublic = false;
|
||||
public $editCountry = null;
|
||||
public $editDivision = null;
|
||||
public $editCity = null;
|
||||
public $editDistrict = null;
|
||||
|
||||
// Additional-language descriptions for inline edit, keyed by locale
|
||||
public array $editTranslationContents = [];
|
||||
public array $editAvailableTagLocales = [];
|
||||
public string $editNewTranslationLocale = '';
|
||||
|
||||
public bool $showEditModal = false;
|
||||
public bool $showDeleteConfirm = false;
|
||||
|
||||
// Admin pause/publish confirmation
|
||||
public bool $showAdminActionConfirm = false;
|
||||
public ?int $adminActionCallId = null;
|
||||
public string $adminActionType = ''; // 'pause' or 'publish'
|
||||
public string $adminActionCallableName = '';
|
||||
|
||||
// Admin bulk delete confirmation
|
||||
public bool $showAdminDeleteConfirm = false;
|
||||
public string $adminDeleteCallableNames = '';
|
||||
|
||||
// Bulk print modal
|
||||
public bool $showBulkPrintModal = false;
|
||||
public int $bulkPrintPerPage = 1;
|
||||
public string $bulkPrintColor = 'bw';
|
||||
|
||||
// Non-admin bulk delete confirmation
|
||||
public bool $showDeleteConfirmModal = false;
|
||||
|
||||
// Per-row delete confirmation
|
||||
public bool $showDeleteRowConfirm = false;
|
||||
|
||||
// Per-row restore confirmation
|
||||
public bool $showRestoreRowConfirm = false;
|
||||
|
||||
protected $listeners = [
|
||||
'countryToParent',
|
||||
'divisionToParent',
|
||||
'cityToParent',
|
||||
'districtToParent',
|
||||
'callTagSelected',
|
||||
'callTagCleared',
|
||||
'callSaved' => '$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');
|
||||
$this->refreshEditAvailableTagLocales();
|
||||
}
|
||||
|
||||
public function callTagCleared(): void
|
||||
{
|
||||
$this->editTagId = null;
|
||||
$this->refreshEditAvailableTagLocales();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute the locales the selected Tag has translations for (excluding the base locale),
|
||||
* and drop any already-added translation whose locale is no longer available.
|
||||
*/
|
||||
public function refreshEditAvailableTagLocales(): void
|
||||
{
|
||||
if (!$this->editTagId) {
|
||||
$this->editAvailableTagLocales = [];
|
||||
$this->editTranslationContents = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$tag = Tag::find($this->editTagId);
|
||||
|
||||
$this->editAvailableTagLocales = $tag
|
||||
? $tag->translations()->pluck('locale')->unique()->reject(fn($locale) => $locale === App::getLocale())->values()->all()
|
||||
: [];
|
||||
|
||||
$this->editTranslationContents = collect($this->editTranslationContents)
|
||||
->only($this->editAvailableTagLocales)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function getEditRemainingTagLocalesProperty(): array
|
||||
{
|
||||
return collect($this->editAvailableTagLocales)
|
||||
->reject(fn($locale) => array_key_exists($locale, $this->editTranslationContents))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function addEditTranslation(): void
|
||||
{
|
||||
if (
|
||||
$this->editNewTranslationLocale
|
||||
&& in_array($this->editNewTranslationLocale, $this->editAvailableTagLocales)
|
||||
&& !array_key_exists($this->editNewTranslationLocale, $this->editTranslationContents)
|
||||
) {
|
||||
$this->editTranslationContents[$this->editNewTranslationLocale] = '';
|
||||
}
|
||||
|
||||
$this->editNewTranslationLocale = '';
|
||||
}
|
||||
|
||||
public function removeEditTranslation(string $locale): void
|
||||
{
|
||||
unset($this->editTranslationContents[$locale]);
|
||||
}
|
||||
|
||||
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->refreshEditAvailableTagLocales();
|
||||
|
||||
// Pre-fill any existing translations for locales the tag currently supports
|
||||
$this->editTranslationContents = $call->translations
|
||||
->whereIn('locale', $this->editAvailableTagLocales)
|
||||
->pluck('content', 'locale')
|
||||
->map(fn($content) => $content ?? '')
|
||||
->all();
|
||||
|
||||
$this->resetValidation();
|
||||
$this->showEditModal = true;
|
||||
|
||||
$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');
|
||||
|
||||
$this->dispatch('manage-edit-opened', [
|
||||
'value' => $this->editTill,
|
||||
'minDate' => now()->addDay()->format('Y/m/d'),
|
||||
'maxDate' => $tillMaxDays !== null ? now()->addDays($tillMaxDays)->format('Y/m/d') : null,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function messages(): array
|
||||
{
|
||||
return [
|
||||
'editTagId.required' => __('Skill tag is required.'),
|
||||
];
|
||||
}
|
||||
|
||||
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',
|
||||
'editTranslationContents.*' => ['nullable', 'string', 'max:' . timebank_config('calls.content_max_input', 200)],
|
||||
];
|
||||
}
|
||||
|
||||
public function saveEdit(): void
|
||||
{
|
||||
$this->authorizeWrite();
|
||||
$this->authorizePermission('update calls');
|
||||
$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,
|
||||
]);
|
||||
|
||||
// Hard-delete all existing translations for this locale before creating a fresh one.
|
||||
// Soft-delete would leave ghost records visible via withTrashed() in the table.
|
||||
CallTranslation::withTrashed()
|
||||
->where('call_id', $call->id)
|
||||
->where('locale', App::getLocale())
|
||||
->forceDelete();
|
||||
|
||||
CallTranslation::create([
|
||||
'call_id' => $call->id,
|
||||
'locale' => App::getLocale(),
|
||||
'content' => $this->editContent ?: null,
|
||||
]);
|
||||
|
||||
foreach ($this->editTranslationContents as $locale => $translatedContent) {
|
||||
CallTranslation::withTrashed()
|
||||
->where('call_id', $call->id)
|
||||
->where('locale', $locale)
|
||||
->forceDelete();
|
||||
|
||||
if ($translatedContent !== '' && $translatedContent !== null) {
|
||||
CallTranslation::create([
|
||||
'call_id' => $call->id,
|
||||
'locale' => $locale,
|
||||
'content' => $translatedContent,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove translations for locales that were removed from the form (but keep the base locale)
|
||||
CallTranslation::withTrashed()
|
||||
->where('call_id', $call->id)
|
||||
->where('locale', '!=', App::getLocale())
|
||||
->whereNotIn('locale', array_keys($this->editTranslationContents))
|
||||
->forceDelete();
|
||||
|
||||
$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 confirmDeleteRow(int $id): void
|
||||
{
|
||||
$this->findCall($id); // authorization check
|
||||
$this->editCallId = $id;
|
||||
$this->showDeleteRowConfirm = true;
|
||||
}
|
||||
|
||||
public function deleteRowCall(): void
|
||||
{
|
||||
$this->authorizePermission('delete calls');
|
||||
$call = $this->findCall($this->editCallId);
|
||||
$call->unsearchable();
|
||||
$call->translations()->delete();
|
||||
$call->delete();
|
||||
|
||||
$this->showDeleteRowConfirm = false;
|
||||
$this->editCallId = null;
|
||||
$this->notification()->success(title: __('Deleted'), description: __('The call has been deleted.'));
|
||||
}
|
||||
|
||||
public function confirmRestoreRow(int $id): void
|
||||
{
|
||||
// Authorization: admin sees all trashed, non-admin scoped to own
|
||||
if ($this->isAdminView) {
|
||||
Call::onlyTrashed()->findOrFail($id);
|
||||
} else {
|
||||
Call::onlyTrashed()
|
||||
->where('callable_type', session('activeProfileType'))
|
||||
->where('callable_id', session('activeProfileId'))
|
||||
->findOrFail($id);
|
||||
}
|
||||
$this->editCallId = $id;
|
||||
$this->showRestoreRowConfirm = true;
|
||||
}
|
||||
|
||||
public function restoreRowCall(): void
|
||||
{
|
||||
$this->authorizePermission('update calls');
|
||||
if ($this->isAdminView) {
|
||||
$call = Call::onlyTrashed()->findOrFail($this->editCallId);
|
||||
} else {
|
||||
$call = Call::onlyTrashed()
|
||||
->where('callable_type', session('activeProfileType'))
|
||||
->where('callable_id', session('activeProfileId'))
|
||||
->findOrFail($this->editCallId);
|
||||
}
|
||||
|
||||
$call->translations()->withTrashed()->restore();
|
||||
$call->restore();
|
||||
$call->update(['is_paused' => true]);
|
||||
$call->unsearchable();
|
||||
|
||||
$this->showRestoreRowConfirm = false;
|
||||
$this->editCallId = null;
|
||||
$this->statusFilter = '';
|
||||
$this->notification()->success(title: __('Restored'), description: __('The call has been restored.'));
|
||||
}
|
||||
|
||||
public function deleteCall(): void
|
||||
{
|
||||
$this->authorizePermission('delete calls');
|
||||
$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 confirmBulkPrint(): void
|
||||
{
|
||||
$this->showBulkPrintModal = true;
|
||||
}
|
||||
|
||||
public function executeBulkPrint(): void
|
||||
{
|
||||
$ids = implode(',', array_map('intval', $this->bulkSelected));
|
||||
$url = route('call.pdf-bulk', [
|
||||
'ids' => $ids,
|
||||
'per_page' => $this->bulkPrintPerPage,
|
||||
'color' => $this->bulkPrintColor,
|
||||
]);
|
||||
$this->showBulkPrintModal = false;
|
||||
$this->dispatch('open-bulk-pdf', url: $url);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
$query = Call::whereIn('id', $this->bulkSelected);
|
||||
if (!$this->isAdminView) {
|
||||
$query->where('callable_type', session('activeProfileType'))
|
||||
->where('callable_id', session('activeProfileId'));
|
||||
}
|
||||
$calls = $query->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();
|
||||
|
||||
$query = Call::onlyTrashed()->whereIn('id', $this->bulkSelected);
|
||||
if (!$this->isAdminView) {
|
||||
$query->where('callable_type', session('activeProfileType'))
|
||||
->where('callable_id', session('activeProfileId'));
|
||||
}
|
||||
$calls = $query->get();
|
||||
foreach ($calls as $call) {
|
||||
$call->translations()->withTrashed()->restore();
|
||||
$call->restore();
|
||||
$call->update(['is_paused' => true]);
|
||||
$call->unsearchable();
|
||||
}
|
||||
|
||||
$this->bulkSelected = [];
|
||||
$this->selectAll = false;
|
||||
$this->statusFilter = '';
|
||||
$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);
|
||||
$this->authorizePermission('unpublish calls');
|
||||
$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);
|
||||
$this->authorizePermission('publish calls');
|
||||
$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();
|
||||
$this->authorizePermission('unpublish calls');
|
||||
$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();
|
||||
$this->authorizePermission('publish calls');
|
||||
$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);
|
||||
}
|
||||
$this->authorizePermission('manage calls');
|
||||
$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);
|
||||
}
|
||||
$this->authorizePermission('manage calls');
|
||||
$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' => fn($q) => $q->withTrashed(),
|
||||
]);
|
||||
|
||||
// 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user