intial ticket fix partial delete fix

This commit is contained in:
Mark Gregory
2026-06-18 14:07:50 +01:00
parent 129eb7a8cd
commit 66b24b65d4
3 changed files with 203 additions and 28 deletions
+18 -14
View File
@@ -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);
}
}