fixe duploads and attachments now with colors tab and enhancements
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Http\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
final class AttachmentDownloadController extends Controller
|
||||
{
|
||||
/**
|
||||
* Download a ticket attachment through Laravel storage instead of a public URL.
|
||||
*
|
||||
* This avoids broken public storage links and keeps ticket authorization enforced.
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function __invoke(int $attachment): StreamedResponse|RedirectResponse
|
||||
{
|
||||
abort_unless(Schema::hasTable('helpdesk_attachments'), 404);
|
||||
|
||||
$row = DB::table('helpdesk_attachments')->where('id', $attachment)->first();
|
||||
|
||||
abort_if($row === null, 404);
|
||||
|
||||
$ticketId = $row->ticket_id ?? null;
|
||||
|
||||
if ($ticketId !== null) {
|
||||
$ticket = Ticket::query()->findOrFail((int) $ticketId);
|
||||
Gate::authorize('view', $ticket);
|
||||
}
|
||||
|
||||
$disk = (string) ($row->disk ?? 'public');
|
||||
$path = (string) ($row->path ?? $row->file_path ?? $row->stored_path ?? '');
|
||||
$name = (string) ($row->original_name
|
||||
?? $row->original_filename
|
||||
?? $row->file_name
|
||||
?? $row->filename
|
||||
?? basename($path));
|
||||
|
||||
abort_if($path === '', 404);
|
||||
abort_unless(Storage::disk($disk)->exists($path), 404);
|
||||
|
||||
return Storage::disk($disk)->download($path, $name);
|
||||
}
|
||||
}
|
||||
@@ -209,37 +209,40 @@ final class TicketEdit extends Component
|
||||
}
|
||||
|
||||
/**
|
||||
* Return assignable users. Falls back to all tenant users when helpdesk flags/roles are absent.
|
||||
* Return assignable users for this ticket.
|
||||
*
|
||||
* The dropdown must never be blank just because optional helpdesk_agent/helpdesk_admin
|
||||
* flags are missing or false. Prefer tenant/company users, then fall back to all users.
|
||||
*
|
||||
* @return EloquentCollection<int, User>
|
||||
*/
|
||||
private function agentOptions(): EloquentCollection
|
||||
{
|
||||
return User::query()
|
||||
->select(['id', 'name', 'email', 'company_id'])
|
||||
->when($this->company_id !== null && Schema::hasColumn('users', 'company_id'), function ($query): void {
|
||||
$query->where(function ($query): void {
|
||||
$columns = ['id', 'name', 'email'];
|
||||
|
||||
if (Schema::hasColumn('users', 'company_id')) {
|
||||
$columns[] = 'company_id';
|
||||
}
|
||||
|
||||
$query = User::query()->select($columns)->orderBy('name');
|
||||
|
||||
if ($this->company_id !== null && Schema::hasColumn('users', 'company_id')) {
|
||||
$tenantUsers = (clone $query)
|
||||
->where(function ($query): void {
|
||||
$query->where('company_id', $this->company_id)
|
||||
->orWhereNull('company_id');
|
||||
});
|
||||
})
|
||||
->when(Schema::hasColumn('users', 'helpdesk_agent') || Schema::hasColumn('users', 'helpdesk_admin'), function ($query): void {
|
||||
$query->where(function ($query): void {
|
||||
if (Schema::hasColumn('users', 'helpdesk_agent')) {
|
||||
$query->orWhere('helpdesk_agent', true);
|
||||
}
|
||||
})
|
||||
->when($this->agent_id !== null, function ($query): void {
|
||||
$query->orWhereKey($this->agent_id);
|
||||
})
|
||||
->get();
|
||||
|
||||
if (Schema::hasColumn('users', 'helpdesk_admin')) {
|
||||
$query->orWhere('helpdesk_admin', true);
|
||||
}
|
||||
if ($tenantUsers->isNotEmpty()) {
|
||||
return $tenantUsers;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->agent_id !== null) {
|
||||
$query->orWhereKey($this->agent_id);
|
||||
}
|
||||
});
|
||||
})
|
||||
->orderBy('name')
|
||||
->get();
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
private function freshTicket(Ticket $ticket): Ticket
|
||||
|
||||
@@ -63,6 +63,7 @@ final class TicketIndex extends Component
|
||||
|
||||
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'))
|
||||
|
||||
@@ -9,6 +9,8 @@ use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
@@ -135,55 +137,124 @@ final class TicketShow extends Component
|
||||
$this->ticket = $this->freshTicket($this->ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the selected ticket status using a direct table update so Livewire UI wrappers cannot silently fail.
|
||||
*/
|
||||
public function updateStatus(WorkflowNotificationService $workflow, SlaService $sla): void
|
||||
{
|
||||
$this->authorize('update', $this->ticket);
|
||||
$this->validate(['status_id' => ['required', 'integer', 'exists:helpdesk_statuses,id']]);
|
||||
|
||||
$status = Status::query()->findOrFail($this->status_id);
|
||||
$this->ticket->forceFill([
|
||||
$statusId = $this->normaliseNullableInt($this->status_id);
|
||||
|
||||
validator(
|
||||
['status_id' => $statusId],
|
||||
['status_id' => ['required', 'integer', Rule::exists('helpdesk_statuses', 'id')]],
|
||||
)->validate();
|
||||
|
||||
$status = Status::query()->findOrFail($statusId);
|
||||
|
||||
$updates = [
|
||||
'status_id' => $status->id,
|
||||
'completed_at' => $status->is_closed ? now() : null,
|
||||
'last_activity_at' => now(),
|
||||
])->save();
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_tickets', 'updated_at')) {
|
||||
$updates['updated_at'] = now();
|
||||
}
|
||||
|
||||
DB::table('helpdesk_tickets')
|
||||
->where('id', $this->ticket->id)
|
||||
->update($updates);
|
||||
|
||||
$this->ticket = $this->freshTicket($this->ticket);
|
||||
$this->status_id = (int) $this->ticket->status_id;
|
||||
|
||||
if ($status->is_closed) {
|
||||
$sla->recordClosure($this->ticket);
|
||||
}
|
||||
|
||||
$this->ticket = $this->freshTicket($this->ticket);
|
||||
$workflow->dispatch($this->ticket, $status->is_closed ? 'ticket.closed' : 'ticket.updated', Auth::user());
|
||||
$this->dispatch('helpdesk-status-updated');
|
||||
|
||||
session()->flash('ticket-action-updated', 'Status updated to '.$this->ticket->status?->name.'.');
|
||||
$this->dispatch('helpdesk-status-updated', statusId: $this->status_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the selected ticket priority using a direct table update.
|
||||
*/
|
||||
public function updatePriority(WorkflowNotificationService $workflow): void
|
||||
{
|
||||
$this->authorize('update', $this->ticket);
|
||||
$this->validate(['priority_id' => ['required', 'integer', 'exists:helpdesk_priorities,id']]);
|
||||
|
||||
$this->ticket->forceFill([
|
||||
'priority_id' => $this->priority_id,
|
||||
$priorityId = $this->normaliseNullableInt($this->priority_id);
|
||||
|
||||
validator(
|
||||
['priority_id' => $priorityId],
|
||||
['priority_id' => ['required', 'integer', Rule::exists('helpdesk_priorities', 'id')]],
|
||||
)->validate();
|
||||
|
||||
$updates = [
|
||||
'priority_id' => $priorityId,
|
||||
'last_activity_at' => now(),
|
||||
])->save();
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_tickets', 'updated_at')) {
|
||||
$updates['updated_at'] = now();
|
||||
}
|
||||
|
||||
DB::table('helpdesk_tickets')
|
||||
->where('id', $this->ticket->id)
|
||||
->update($updates);
|
||||
|
||||
$this->ticket = $this->freshTicket($this->ticket);
|
||||
$this->priority_id = (int) $this->ticket->priority_id;
|
||||
|
||||
$workflow->dispatch($this->ticket, 'ticket.updated', Auth::user());
|
||||
$this->dispatch('helpdesk-priority-updated');
|
||||
|
||||
session()->flash('ticket-action-updated', 'Priority updated to '.$this->ticket->priority?->name.'.');
|
||||
$this->dispatch('helpdesk-priority-updated', priorityId: $this->priority_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the selected ticket assignee.
|
||||
*/
|
||||
public function updateAgent(WorkflowNotificationService $workflow): void
|
||||
{
|
||||
$this->authorize('update', $this->ticket);
|
||||
$this->validate(['agent_id' => ['nullable', 'integer', 'exists:users,id']]);
|
||||
|
||||
$this->ticket->forceFill([
|
||||
'agent_id' => $this->agent_id,
|
||||
$agentId = $this->normaliseNullableInt($this->agent_id);
|
||||
|
||||
validator(
|
||||
['agent_id' => $agentId],
|
||||
['agent_id' => ['nullable', 'integer', Rule::exists('users', 'id')]],
|
||||
)->validate();
|
||||
|
||||
abort_unless(Schema::hasColumn('helpdesk_tickets', 'agent_id'), 500, 'The helpdesk_tickets.agent_id column is missing.');
|
||||
|
||||
$updates = [
|
||||
'agent_id' => $agentId,
|
||||
'last_activity_at' => now(),
|
||||
])->save();
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_tickets', 'updated_at')) {
|
||||
$updates['updated_at'] = now();
|
||||
}
|
||||
|
||||
DB::table('helpdesk_tickets')
|
||||
->where('id', $this->ticket->id)
|
||||
->update($updates);
|
||||
|
||||
$this->ticket = $this->freshTicket($this->ticket);
|
||||
$this->agent_id = $this->ticket->agent_id === null ? null : (int) $this->ticket->agent_id;
|
||||
|
||||
$workflow->dispatch($this->ticket, 'ticket.updated', Auth::user());
|
||||
$this->dispatch('helpdesk-agent-updated');
|
||||
|
||||
session()->flash('ticket-action-updated', $this->agent_id === null
|
||||
? 'Ticket is now unassigned.'
|
||||
: 'Ticket assigned to '.$this->ticket->agent?->name.'.');
|
||||
|
||||
$this->dispatch('helpdesk-agent-updated', agentId: $this->agent_id);
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
@@ -196,6 +267,18 @@ final class TicketShow extends Component
|
||||
])->layout('layouts.app');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Livewire select values into a nullable integer.
|
||||
*/
|
||||
private function normaliseNullableInt(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '' || $value === '0' || $value === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
private function freshTicket(Ticket $ticket): Ticket
|
||||
{
|
||||
$ticket = $ticket->fresh([
|
||||
@@ -228,45 +311,42 @@ final class TicketShow extends Component
|
||||
}
|
||||
|
||||
/**
|
||||
* Return assignable users. Falls back to all tenant users when helpdesk flags/roles are absent.
|
||||
* Return assignable users for this ticket.
|
||||
*
|
||||
* The dropdown must never be blank just because optional helpdesk_agent/helpdesk_admin
|
||||
* flags are missing or false. Prefer tenant/company users, then fall back to all users.
|
||||
*
|
||||
* @return EloquentCollection<int, User>
|
||||
*/
|
||||
private function agentOptions(): EloquentCollection
|
||||
{
|
||||
return User::query()
|
||||
->select(['id', 'name', 'email', 'company_id'])
|
||||
->when($this->ticket->company_id !== null && Schema::hasColumn('users', 'company_id'), function ($query): void {
|
||||
$query->where(function ($query): void {
|
||||
$columns = ['id', 'name', 'email'];
|
||||
|
||||
if (Schema::hasColumn('users', 'company_id')) {
|
||||
$columns[] = 'company_id';
|
||||
}
|
||||
|
||||
$query = User::query()->select($columns)->orderBy('name');
|
||||
|
||||
if ($this->ticket->company_id !== null && Schema::hasColumn('users', 'company_id')) {
|
||||
$tenantUsers = (clone $query)
|
||||
->where(function ($query): void {
|
||||
$query->where('company_id', $this->ticket->company_id)
|
||||
->orWhereNull('company_id');
|
||||
});
|
||||
})
|
||||
->when(Schema::hasColumn('users', 'helpdesk_agent') || Schema::hasColumn('users', 'helpdesk_admin'), function ($query): void {
|
||||
$query->where(function ($query): void {
|
||||
if (Schema::hasColumn('users', 'helpdesk_agent')) {
|
||||
$query->orWhere('helpdesk_agent', true);
|
||||
}
|
||||
})
|
||||
->when($this->agent_id !== null, function ($query): void {
|
||||
$query->orWhereKey($this->agent_id);
|
||||
})
|
||||
->get();
|
||||
|
||||
if (Schema::hasColumn('users', 'helpdesk_admin')) {
|
||||
$query->orWhere('helpdesk_admin', true);
|
||||
}
|
||||
if ($tenantUsers->isNotEmpty()) {
|
||||
return $tenantUsers;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->agent_id !== null) {
|
||||
$query->orWhereKey($this->agent_id);
|
||||
}
|
||||
});
|
||||
})
|
||||
->orderBy('name')
|
||||
->get();
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an unlimited nested comment tree from a flat eager-loaded collection.
|
||||
*
|
||||
* @param Collection<int|string, Collection<int, mixed>> $grouped
|
||||
* @return Collection<int, mixed>
|
||||
*/
|
||||
private function buildCommentTree(Collection $grouped, ?int $parentId = null): Collection
|
||||
{
|
||||
return $grouped->get($parentId, collect())->map(function ($comment) use ($grouped) {
|
||||
|
||||
@@ -381,6 +381,7 @@
|
||||
Uploading attachments...
|
||||
</div>
|
||||
|
||||
@if ($ticket->attachments->isNotEmpty())
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-3">
|
||||
<h3 class="inline-flex items-center gap-2 text-sm font-extrabold text-emerald-200">
|
||||
@@ -396,10 +397,10 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@forelse ($ticket->attachments as $attachment)
|
||||
@foreach ($ticket->attachments as $attachment)
|
||||
<div class="flex flex-col gap-3 rounded-xl border border-zinc-700 bg-zinc-900 p-3 text-sm sm:flex-row sm:items-center sm:justify-between">
|
||||
<a
|
||||
href="{{ method_exists($attachment, 'url') ? $attachment->url() : Storage::disk($attachment->disk ?? 'public')->url($attachment->path ?? $attachment->file_path) }}"
|
||||
href="{{ route('helpdesk.attachments.download', $attachment->id) }}"
|
||||
target="_blank"
|
||||
class="min-w-0 font-medium text-blue-300 hover:text-blue-200"
|
||||
>
|
||||
@@ -416,12 +417,9 @@
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed border-zinc-700 bg-zinc-900 px-4 py-3 text-sm text-zinc-400">
|
||||
No ticket-level attachments are currently saved.
|
||||
</div>
|
||||
@endforelse
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($attachments !== [])
|
||||
<div class="mt-4 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-3 text-sm text-blue-100">
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Priority</th>
|
||||
<th class="px-4 py-3">Agent</th>
|
||||
<th class="px-4 py-3 text-center">Files</th>
|
||||
<th class="px-4 py-3">Last activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -216,11 +217,25 @@
|
||||
<td class="px-4 py-3"><flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}</flux:badge></td>
|
||||
<td class="px-4 py-3"><flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}</flux:badge></td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $ticket->agent?->name ?? 'Unassigned' }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-center">
|
||||
@if (($ticket->attachments_count ?? 0) > 0)
|
||||
<span class="inline-flex items-center justify-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-black text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200" title="{{ $ticket->attachments_count }} attachment{{ $ticket->attachments_count === 1 ? '' : 's' }}">
|
||||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||||
<span>{{ $ticket->attachments_count }}</span>
|
||||
<span class="sr-only">attachment{{ $ticket->attachments_count === 1 ? '' : 's' }}</span>
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center justify-center rounded-full border border-zinc-200 bg-zinc-50 px-2.5 py-1 text-xs font-bold text-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-500" title="No attachments">
|
||||
<i class="fa-regular fa-circle" aria-hidden="true"></i>
|
||||
<span class="sr-only">No attachments</span>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-zinc-500">{{ $ticket->last_activity_at?->diffForHumans() ?? $ticket->created_at->diffForHumans() }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-12 text-center text-zinc-500">No tickets matched your filters.</td>
|
||||
<td colspan="7" class="px-4 py-12 text-center text-zinc-500">No tickets matched your filters.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@if ($ticket->attachments->isNotEmpty())
|
||||
<div class="overflow-hidden rounded-2xl border border-blue-200 bg-white shadow-sm dark:border-blue-900/60 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-r from-blue-700 via-sky-600 to-emerald-600 px-6 py-4 text-white">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
@@ -59,18 +60,15 @@
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 p-6 sm:grid-cols-2">
|
||||
@forelse ($ticket->attachments as $attachment)
|
||||
<a href="{{ method_exists($attachment, 'url') ? $attachment->url() : Storage::disk($attachment->disk ?? 'public')->url($attachment->path ?? $attachment->file_path) }}" target="_blank" class="rounded-xl border border-zinc-200 p-3 text-sm hover:bg-zinc-50 dark:border-zinc-700 dark:hover:bg-zinc-800">
|
||||
@foreach ($ticket->attachments as $attachment)
|
||||
<a href="{{ route('helpdesk.attachments.download', $attachment->id) }}" target="_blank" class="rounded-xl border border-zinc-200 p-3 text-sm hover:bg-zinc-50 dark:border-zinc-700 dark:hover:bg-zinc-800">
|
||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $attachment->original_name ?? $attachment->original_filename ?? $attachment->filename ?? $attachment->file_name ?? basename($attachment->path ?? $attachment->file_path ?? '') }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ Number::fileSize($attachment->size ?? $attachment->size_bytes ?? 0) }}</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed border-zinc-300 p-4 text-sm text-zinc-500 dark:border-zinc-700">
|
||||
No ticket-level attachments are currently saved.
|
||||
</div>
|
||||
@endforelse
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Conversation</h2>
|
||||
@@ -204,37 +202,65 @@
|
||||
|
||||
@can('update', $ticket)
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Agent actions</h2>
|
||||
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-200" for="show_agent_id">Assigned agent</label>
|
||||
<select
|
||||
id="show_agent_id"
|
||||
wire:model.live="agent_id"
|
||||
class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-sm dark:border-zinc-700 dark:bg-zinc-950"
|
||||
data-dusk="ticket-show-agent"
|
||||
>
|
||||
<option value="">Unassigned</option>
|
||||
@forelse ($agents as $agent)
|
||||
<option value="{{ $agent->id }}">{{ $agent->name }} · {{ $agent->email }}</option>
|
||||
@empty
|
||||
<option value="" disabled>No users available for assignment</option>
|
||||
@endforelse
|
||||
</select>
|
||||
<flux:button dusk="update-agent" wire:click="updateAgent" class="mt-4 mb-5 w-full !bg-emerald-600 !text-white hover:!bg-emerald-700 focus:!ring-emerald-400/60" variant="primary" icon="user">Update assigned agent</flux:button>
|
||||
<div class="mb-5 rounded-2xl bg-gradient-to-r from-blue-700 via-indigo-700 to-violet-700 px-4 py-4 text-white shadow-sm">
|
||||
<p class="text-xs font-black uppercase tracking-[0.24em] text-blue-100">Ticket controls</p>
|
||||
<h2 class="mt-1 text-lg font-black tracking-tight">Assignment, status and priority</h2>
|
||||
<p class="mt-1 text-sm font-semibold text-blue-100/90">Use the explicit update buttons below to save each change.</p>
|
||||
</div>
|
||||
|
||||
<flux:select wire:model="status_id" label="Status">
|
||||
@foreach ($statuses as $status)
|
||||
<option value="{{ $status->id }}">{{ $status->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:button dusk="update-status" wire:click="updateStatus" class="mt-4 w-full !bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60" variant="primary" icon="check">Update status</flux:button>
|
||||
<div class="mt-5">
|
||||
<flux:select wire:model="priority_id" label="Priority">
|
||||
@if (session('ticket-action-updated'))
|
||||
<div class="mb-5 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm font-bold text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-100">
|
||||
{{ session('ticket-action-updated') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form wire:submit.prevent="updateAgent" class="mb-6 space-y-3">
|
||||
<label class="block text-sm font-bold text-zinc-700 dark:text-zinc-200" for="show_agent_id">Assigned agent</label>
|
||||
<select id="show_agent_id" wire:model.live="agent_id" class="hd-select block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm font-bold text-zinc-900 shadow-sm focus:border-emerald-500 focus:ring-emerald-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" data-dusk="ticket-show-agent">
|
||||
<option value="">Unassigned</option>
|
||||
@forelse ($agents as $agent)
|
||||
<option value="{{ $agent->id }}">{{ $agent->name }} · {{ $agent->email }}</option>
|
||||
@empty
|
||||
<option value="" disabled>No users available for assignment</option>
|
||||
@endforelse
|
||||
</select>
|
||||
@error('agent_id') <p class="text-sm font-bold text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
|
||||
<button type="submit" dusk="update-agent" class="inline-flex w-full items-center justify-center rounded-xl bg-emerald-600 px-4 py-2.5 text-sm font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70 disabled:cursor-not-allowed disabled:opacity-70" wire:loading.attr="disabled" wire:target="updateAgent">
|
||||
<i class="fa-solid fa-user-check mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="updateAgent">Assign agent</span>
|
||||
<span wire:loading wire:target="updateAgent">Assigning...</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form wire:submit.prevent="updateStatus" class="mb-6 space-y-3">
|
||||
<label class="block text-sm font-bold text-zinc-700 dark:text-zinc-200" for="show_status_id">Status</label>
|
||||
<select id="show_status_id" wire:model.live="status_id" class="hd-select block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm font-bold text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" data-dusk="ticket-show-status">
|
||||
@foreach ($statuses as $status)
|
||||
<option value="{{ $status->id }}">{{ $status->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('status_id') <p class="text-sm font-bold text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
|
||||
<button type="submit" dusk="update-status" class="inline-flex w-full items-center justify-center rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-black text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/70 disabled:cursor-not-allowed disabled:opacity-70" wire:loading.attr="disabled" wire:target="updateStatus">
|
||||
<i class="fa-solid fa-circle-check mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="updateStatus">Update status</span>
|
||||
<span wire:loading wire:target="updateStatus">Updating...</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form wire:submit.prevent="updatePriority" class="space-y-3">
|
||||
<label class="block text-sm font-bold text-zinc-700 dark:text-zinc-200" for="show_priority_id">Priority</label>
|
||||
<select id="show_priority_id" wire:model.live="priority_id" class="hd-select block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm font-bold text-zinc-900 shadow-sm focus:border-amber-500 focus:ring-amber-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" data-dusk="ticket-show-priority">
|
||||
@foreach ($priorities as $priority)
|
||||
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:button dusk="update-priority" wire:click="updatePriority" class="mt-4 w-full !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60" variant="ghost" icon="flag">Update priority</flux:button>
|
||||
</div>
|
||||
</select>
|
||||
@error('priority_id') <p class="text-sm font-bold text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
|
||||
<button type="submit" dusk="update-priority" class="inline-flex w-full items-center justify-center rounded-xl bg-amber-500 px-4 py-2.5 text-sm font-black text-white shadow-sm transition hover:bg-amber-600 focus:outline-none focus:ring-2 focus:ring-amber-300/70 disabled:cursor-not-allowed disabled:opacity-70" wire:loading.attr="disabled" wire:target="updatePriority">
|
||||
<i class="fa-solid fa-flag mr-2" aria-hidden="true"></i>
|
||||
<span wire:loading.remove wire:target="updatePriority">Update priority</span>
|
||||
<span wire:loading wire:target="updatePriority">Updating...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endcan
|
||||
</aside>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController;
|
||||
use Modules\Helpdesk\Http\Controllers\HelpdeskController;
|
||||
use Modules\Helpdesk\Http\Controllers\TicketStoreController;
|
||||
use Modules\Helpdesk\Http\Controllers\WorkflowOpenController;
|
||||
@@ -25,3 +26,6 @@ Route::post('/tickets', TicketStoreController::class)->name('tickets.store');
|
||||
Route::get('/tickets/create', TicketCreate::class)->name('tickets.create');
|
||||
Route::get('/tickets/{ticket}/edit', TicketEdit::class)->name('tickets.edit');
|
||||
Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');
|
||||
Route::get('/attachments/{attachment}/download', AttachmentDownloadController::class)
|
||||
->whereNumber('attachment')
|
||||
->name('attachments.download');
|
||||
|
||||
Reference in New Issue
Block a user