intial ticket fix partial delete fix
This commit is contained in:
@@ -21,11 +21,13 @@ final class TicketIndex extends Component
|
|||||||
use AuthorizesRequests;
|
use AuthorizesRequests;
|
||||||
use WithPagination;
|
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]
|
#[Url]
|
||||||
public string $search = '';
|
public string $search = '';
|
||||||
@@ -47,18 +49,19 @@ final class TicketIndex extends Component
|
|||||||
/**
|
/**
|
||||||
* Open the delete confirmation modal for a visible ticket.
|
* Open the delete confirmation modal for a visible ticket.
|
||||||
*/
|
*/
|
||||||
public function confirmDeleteTicket(int $ticketId): void
|
public function askToDeleteTicket(int $ticketId): void
|
||||||
{
|
{
|
||||||
$ticket = Ticket::query()
|
$ticket = Ticket::query()
|
||||||
->visibleTo(Auth::user())
|
->visibleTo(Auth::user())
|
||||||
->findOrFail($ticketId);
|
->findOrFail($ticketId);
|
||||||
|
|
||||||
$this->ticketDeletionId = $ticket->id;
|
$this->pendingDeleteTicketId = $ticket->id;
|
||||||
$this->ticketDeletionReference = $ticket->reference;
|
$this->pendingDeleteTicketReference = $ticket->reference;
|
||||||
$this->confirmingTicketDeletion = true;
|
$this->pendingDeleteTicketSubject = $ticket->subject;
|
||||||
|
$this->deleteModalOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cancelDeleteTicket(): void
|
public function cancelTicketDelete(): void
|
||||||
{
|
{
|
||||||
$this->resetTicketDeletionState();
|
$this->resetTicketDeletionState();
|
||||||
}
|
}
|
||||||
@@ -66,9 +69,9 @@ final class TicketIndex extends Component
|
|||||||
/**
|
/**
|
||||||
* Delete the confirmed ticket.
|
* Delete the confirmed ticket.
|
||||||
*/
|
*/
|
||||||
public function deleteConfirmedTicket(): void
|
public function confirmTicketDelete(): void
|
||||||
{
|
{
|
||||||
if ($this->ticketDeletionId === null) {
|
if ($this->pendingDeleteTicketId === null) {
|
||||||
$this->resetTicketDeletionState();
|
$this->resetTicketDeletionState();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -76,7 +79,7 @@ final class TicketIndex extends Component
|
|||||||
|
|
||||||
$ticket = Ticket::query()
|
$ticket = Ticket::query()
|
||||||
->visibleTo(Auth::user())
|
->visibleTo(Auth::user())
|
||||||
->findOrFail($this->ticketDeletionId);
|
->findOrFail($this->pendingDeleteTicketId);
|
||||||
|
|
||||||
$this->authorize('delete', $ticket);
|
$this->authorize('delete', $ticket);
|
||||||
|
|
||||||
@@ -106,9 +109,10 @@ final class TicketIndex extends Component
|
|||||||
|
|
||||||
private function resetTicketDeletionState(): void
|
private function resetTicketDeletionState(): void
|
||||||
{
|
{
|
||||||
$this->confirmingTicketDeletion = false;
|
$this->deleteModalOpen = false;
|
||||||
$this->ticketDeletionId = null;
|
$this->pendingDeleteTicketId = null;
|
||||||
$this->ticketDeletionReference = null;
|
$this->pendingDeleteTicketReference = null;
|
||||||
|
$this->pendingDeleteTicketSubject = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
<div class="hd-helpdesk-surface space-y-6" dusk="helpdesk-ticket-index">
|
||||||
<style>
|
<style>
|
||||||
.hd-helpdesk-surface .hd-btn,
|
.hd-helpdesk-surface .hd-btn,
|
||||||
.hd-btn {
|
.hd-btn {
|
||||||
@@ -136,7 +136,6 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</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="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="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">
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
@@ -158,6 +157,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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="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">
|
<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" />
|
<flux:input wire:model.live.debounce.350ms="search" icon="magnifying-glass" placeholder="Search reference, subject or body" />
|
||||||
@@ -237,8 +237,10 @@
|
|||||||
@can('delete', $ticket)
|
@can('delete', $ticket)
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
wire:click="confirmDeleteTicket({{ $ticket->id }})"
|
wire:click="askToDeleteTicket({{ $ticket->id }})"
|
||||||
class="hd-btn hd-btn-sm hd-btn-red"
|
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 }}"
|
title="Delete {{ $ticket->reference }}"
|
||||||
dusk="delete-ticket-{{ $ticket->id }}"
|
dusk="delete-ticket-{{ $ticket->id }}"
|
||||||
>
|
>
|
||||||
@@ -261,12 +263,14 @@
|
|||||||
{{ $tickets->links() }}
|
{{ $tickets->links() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if ($confirmingTicketDeletion)
|
|
||||||
|
@if ($deleteModalOpen)
|
||||||
<div
|
<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"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="delete-ticket-modal-title"
|
aria-labelledby="delete-ticket-modal-title"
|
||||||
|
wire:key="ticket-delete-modal-{{ $pendingDeleteTicketId ?? 'none' }}"
|
||||||
dusk="delete-ticket-confirmation-modal"
|
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">
|
<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>
|
</h2>
|
||||||
|
|
||||||
<p class="mt-1 text-sm leading-6 text-red-100/90">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<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>
|
</p>
|
||||||
</div>
|
</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">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
wire:click="cancelDeleteTicket"
|
wire:click="cancelTicketDelete"
|
||||||
class="hd-btn hd-btn-sm hd-btn-blue"
|
class="hd-btn hd-btn-sm hd-btn-blue"
|
||||||
dusk="cancel-delete-ticket"
|
dusk="cancel-delete-ticket"
|
||||||
>
|
>
|
||||||
@@ -307,15 +318,15 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
wire:click="deleteConfirmedTicket"
|
wire:click="confirmTicketDelete"
|
||||||
wire:loading.attr="disabled"
|
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"
|
class="hd-btn hd-btn-sm hd-btn-red disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
dusk="confirm-delete-ticket"
|
dusk="confirm-delete-ticket"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||||
<span wire:loading.remove wire:target="deleteConfirmedTicket">Delete ticket</span>
|
<span wire:loading.remove wire:target="confirmTicketDelete">Delete ticket</span>
|
||||||
<span wire:loading wire:target="deleteConfirmedTicket">Deleting...</span>
|
<span wire:loading wire:target="confirmTicketDelete">Deleting...</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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