intial ticket fix partial delete fix
This commit is contained in:
@@ -21,11 +21,13 @@ final class TicketIndex extends Component
|
||||
use AuthorizesRequests;
|
||||
use WithPagination;
|
||||
|
||||
public bool $confirmingTicketDeletion = false;
|
||||
public bool $deleteModalOpen = false;
|
||||
|
||||
public ?int $ticketDeletionId = null;
|
||||
public ?int $pendingDeleteTicketId = null;
|
||||
|
||||
public ?string $ticketDeletionReference = null;
|
||||
public ?string $pendingDeleteTicketReference = null;
|
||||
|
||||
public ?string $pendingDeleteTicketSubject = null;
|
||||
|
||||
#[Url]
|
||||
public string $search = '';
|
||||
@@ -47,18 +49,19 @@ final class TicketIndex extends Component
|
||||
/**
|
||||
* Open the delete confirmation modal for a visible ticket.
|
||||
*/
|
||||
public function confirmDeleteTicket(int $ticketId): void
|
||||
public function askToDeleteTicket(int $ticketId): void
|
||||
{
|
||||
$ticket = Ticket::query()
|
||||
->visibleTo(Auth::user())
|
||||
->findOrFail($ticketId);
|
||||
|
||||
$this->ticketDeletionId = $ticket->id;
|
||||
$this->ticketDeletionReference = $ticket->reference;
|
||||
$this->confirmingTicketDeletion = true;
|
||||
$this->pendingDeleteTicketId = $ticket->id;
|
||||
$this->pendingDeleteTicketReference = $ticket->reference;
|
||||
$this->pendingDeleteTicketSubject = $ticket->subject;
|
||||
$this->deleteModalOpen = true;
|
||||
}
|
||||
|
||||
public function cancelDeleteTicket(): void
|
||||
public function cancelTicketDelete(): void
|
||||
{
|
||||
$this->resetTicketDeletionState();
|
||||
}
|
||||
@@ -66,9 +69,9 @@ final class TicketIndex extends Component
|
||||
/**
|
||||
* Delete the confirmed ticket.
|
||||
*/
|
||||
public function deleteConfirmedTicket(): void
|
||||
public function confirmTicketDelete(): void
|
||||
{
|
||||
if ($this->ticketDeletionId === null) {
|
||||
if ($this->pendingDeleteTicketId === null) {
|
||||
$this->resetTicketDeletionState();
|
||||
|
||||
return;
|
||||
@@ -76,7 +79,7 @@ final class TicketIndex extends Component
|
||||
|
||||
$ticket = Ticket::query()
|
||||
->visibleTo(Auth::user())
|
||||
->findOrFail($this->ticketDeletionId);
|
||||
->findOrFail($this->pendingDeleteTicketId);
|
||||
|
||||
$this->authorize('delete', $ticket);
|
||||
|
||||
@@ -106,9 +109,10 @@ final class TicketIndex extends Component
|
||||
|
||||
private function resetTicketDeletionState(): void
|
||||
{
|
||||
$this->confirmingTicketDeletion = false;
|
||||
$this->ticketDeletionId = null;
|
||||
$this->ticketDeletionReference = null;
|
||||
$this->deleteModalOpen = false;
|
||||
$this->pendingDeleteTicketId = null;
|
||||
$this->pendingDeleteTicketReference = null;
|
||||
$this->pendingDeleteTicketSubject = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-index">
|
||||
<style>
|
||||
.hd-helpdesk-surface .hd-btn,
|
||||
.hd-btn {
|
||||
@@ -136,7 +136,6 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-index">
|
||||
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
@@ -158,6 +157,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
<flux:input wire:model.live.debounce.350ms="search" icon="magnifying-glass" placeholder="Search reference, subject or body" />
|
||||
@@ -237,8 +237,10 @@
|
||||
@can('delete', $ticket)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="confirmDeleteTicket({{ $ticket->id }})"
|
||||
class="hd-btn hd-btn-sm hd-btn-red"
|
||||
wire:click="askToDeleteTicket({{ $ticket->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="askToDeleteTicket({{ $ticket->id }})"
|
||||
class="hd-btn hd-btn-sm hd-btn-red disabled:cursor-not-allowed disabled:opacity-60"
|
||||
title="Delete {{ $ticket->reference }}"
|
||||
dusk="delete-ticket-{{ $ticket->id }}"
|
||||
>
|
||||
@@ -261,12 +263,14 @@
|
||||
{{ $tickets->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@if ($confirmingTicketDeletion)
|
||||
|
||||
@if ($deleteModalOpen)
|
||||
<div
|
||||
class="fixed inset-0 z-[9998] flex items-center justify-center bg-black/70 px-4 py-6 backdrop-blur-sm"
|
||||
class="fixed inset-0 z-[9998] flex items-center justify-center bg-black/75 px-4 py-6 backdrop-blur-sm"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="delete-ticket-modal-title"
|
||||
wire:key="ticket-delete-modal-{{ $pendingDeleteTicketId ?? 'none' }}"
|
||||
dusk="delete-ticket-confirmation-modal"
|
||||
>
|
||||
<div class="w-full max-w-lg overflow-hidden rounded-3xl border border-red-500/30 bg-zinc-950 text-zinc-100 shadow-2xl">
|
||||
@@ -282,22 +286,29 @@
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm leading-6 text-red-100/90">
|
||||
This will delete <span class="font-mono font-bold">{{ $ticketDeletionReference }}</span>. This action cannot be undone.
|
||||
This will delete <span class="font-mono font-bold">{{ $pendingDeleteTicketReference }}</span>. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-5">
|
||||
<div class="space-y-3 px-6 py-5">
|
||||
@if ($pendingDeleteTicketSubject)
|
||||
<div class="rounded-2xl border border-zinc-800 bg-zinc-900 px-4 py-3">
|
||||
<p class="text-xs font-bold uppercase tracking-[0.22em] text-zinc-500">Ticket subject</p>
|
||||
<p class="mt-1 text-sm font-semibold text-zinc-100">{{ $pendingDeleteTicketSubject }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-sm leading-6 text-zinc-300">
|
||||
Confirm only if this ticket was created in error or is no longer required. Related ticket data may also be removed depending on the model relationships.
|
||||
Confirm only if this ticket was created in error or is no longer required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col-reverse gap-3 border-t border-zinc-800 bg-zinc-900/80 px-6 py-4 sm:flex-row sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="cancelDeleteTicket"
|
||||
wire:click="cancelTicketDelete"
|
||||
class="hd-btn hd-btn-sm hd-btn-blue"
|
||||
dusk="cancel-delete-ticket"
|
||||
>
|
||||
@@ -307,15 +318,15 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="deleteConfirmedTicket"
|
||||
wire:click="confirmTicketDelete"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="deleteConfirmedTicket"
|
||||
wire:target="confirmTicketDelete"
|
||||
class="hd-btn hd-btn-sm hd-btn-red disabled:cursor-not-allowed disabled:opacity-60"
|
||||
dusk="confirm-delete-ticket"
|
||||
>
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="deleteConfirmedTicket">Delete ticket</span>
|
||||
<span wire:loading wire:target="deleteConfirmedTicket">Deleting...</span>
|
||||
<span wire:loading.remove wire:target="confirmTicketDelete">Delete ticket</span>
|
||||
<span wire:loading wire:target="confirmTicketDelete">Deleting...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Livewire;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Modules\Helpdesk\Models\Priority;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
|
||||
final class TicketIndex extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use WithPagination;
|
||||
|
||||
public bool $deleteModalOpen = false;
|
||||
|
||||
public ?int $pendingDeleteTicketId = null;
|
||||
|
||||
public ?string $pendingDeleteTicketReference = null;
|
||||
|
||||
public ?string $pendingDeleteTicketSubject = null;
|
||||
|
||||
#[Url]
|
||||
public string $search = '';
|
||||
|
||||
#[Url]
|
||||
public string $status = '';
|
||||
|
||||
#[Url]
|
||||
public string $priority = '';
|
||||
|
||||
#[Url]
|
||||
public string $scope = 'open';
|
||||
|
||||
/**
|
||||
* Reset pagination when filters change.
|
||||
*/
|
||||
public function updated(string $property): void
|
||||
{
|
||||
if (in_array($property, ['search', 'status', 'priority', 'scope'], true)) {
|
||||
$this->resetPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the delete confirmation modal for the selected visible ticket.
|
||||
*/
|
||||
public function askToDeleteTicket(int $ticketId): void
|
||||
{
|
||||
$ticket = Ticket::query()
|
||||
->visibleTo(Auth::user())
|
||||
->findOrFail($ticketId);
|
||||
|
||||
$this->pendingDeleteTicketId = $ticket->id;
|
||||
$this->pendingDeleteTicketReference = $ticket->reference;
|
||||
$this->pendingDeleteTicketSubject = $ticket->subject;
|
||||
$this->deleteModalOpen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the delete confirmation modal without deleting anything.
|
||||
*/
|
||||
public function cancelTicketDelete(): void
|
||||
{
|
||||
$this->resetDeleteModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the selected ticket after explicit modal confirmation.
|
||||
*/
|
||||
public function confirmTicketDelete(): void
|
||||
{
|
||||
if ($this->pendingDeleteTicketId === null) {
|
||||
$this->resetDeleteModal();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$ticket = Ticket::query()
|
||||
->visibleTo(Auth::user())
|
||||
->findOrFail($this->pendingDeleteTicketId);
|
||||
|
||||
$this->authorize('delete', $ticket);
|
||||
|
||||
$reference = $ticket->reference;
|
||||
|
||||
$ticket->delete();
|
||||
|
||||
$this->resetDeleteModal();
|
||||
$this->resetPage();
|
||||
|
||||
$this->dispatch(
|
||||
'wms:toast',
|
||||
type: 'success',
|
||||
title: 'Ticket deleted',
|
||||
message: "Ticket {$reference} was deleted successfully.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the ticket list.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('helpdesk::livewire.ticket-index', [
|
||||
'tickets' => $this->tickets(),
|
||||
'statuses' => Status::query()->orderBy('sort_order')->get(),
|
||||
'priorities' => Priority::query()->orderBy('sort_order')->get(),
|
||||
])->layout('layouts.app');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset delete modal state.
|
||||
*/
|
||||
private function resetDeleteModal(): void
|
||||
{
|
||||
$this->deleteModalOpen = false;
|
||||
$this->pendingDeleteTicketId = null;
|
||||
$this->pendingDeleteTicketReference = null;
|
||||
$this->pendingDeleteTicketSubject = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build paginated ticket results.
|
||||
*
|
||||
* @return LengthAwarePaginator<int, Ticket>
|
||||
*/
|
||||
private function tickets(): LengthAwarePaginator
|
||||
{
|
||||
$user = Auth::user();
|
||||
$search = trim($this->search);
|
||||
|
||||
return Ticket::query()
|
||||
->with(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team'])
|
||||
->withCount('attachments')
|
||||
->visibleTo($user)
|
||||
->when($this->scope === 'open', static fn (Builder $query): Builder => $query->whereNull('completed_at'))
|
||||
->when($this->scope === 'closed', static fn (Builder $query): Builder => $query->whereNotNull('completed_at'))
|
||||
->when($this->status !== '', fn (Builder $query): Builder => $query->where('status_id', (int) $this->status))
|
||||
->when($this->priority !== '', fn (Builder $query): Builder => $query->where('priority_id', (int) $this->priority))
|
||||
->when($search !== '', static function (Builder $query) use ($search): Builder {
|
||||
return $query->where(static function (Builder $subQuery) use ($search): void {
|
||||
$subQuery->where('reference', 'like', '%'.$search.'%')
|
||||
->orWhere('subject', 'like', '%'.$search.'%')
|
||||
->orWhere('body', 'like', '%'.$search.'%');
|
||||
});
|
||||
})
|
||||
->latest('last_activity_at')
|
||||
->paginate(15);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user