Email integration: IMAP polling via webklex/php-imap (M365/Zoho/MIAB
compatible), TicketEmailProcessor with 3-strategy ticket matching
(In-Reply-To, References, subject line [EZSCALE-{id}]), signature and
quote stripping, scheduled command every 2min.
Outbound: 4 notification classes (TicketCreated, CustomerReply,
StaffReply, StatusChanged) with email threading headers via
withSymfonyMessage(). Staff replies set Reply-To: support@ezscale.cloud.
Frontend: ticket reference chips on all pages, "Via Email" badges on
email-originated replies. 12 new tests (163 total, 816 assertions).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
139 lines
4.4 KiB
PHP
139 lines
4.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\AuditLog;
|
|
use App\Models\SupportTicket;
|
|
use App\Models\TicketReply;
|
|
use App\Notifications\TicketStaffReplyNotification;
|
|
use App\Notifications\TicketStatusChangedNotification;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class TicketController extends Controller
|
|
{
|
|
public function index(Request $request): Response
|
|
{
|
|
$query = SupportTicket::query()
|
|
->with('user:id,name,email');
|
|
|
|
if ($search = $request->input('search')) {
|
|
$query->where(function ($q) use ($search): void {
|
|
$q->where('subject', 'like', "%{$search}%")
|
|
->orWhereHas('user', function ($uq) use ($search): void {
|
|
$uq->where('name', 'like', "%{$search}%")
|
|
->orWhere('email', 'like', "%{$search}%");
|
|
});
|
|
});
|
|
}
|
|
|
|
if ($status = $request->input('status')) {
|
|
$query->where('status', $status);
|
|
}
|
|
|
|
if ($priority = $request->input('priority')) {
|
|
$query->where('priority', $priority);
|
|
}
|
|
|
|
if ($department = $request->input('department')) {
|
|
$query->where('department', $department);
|
|
}
|
|
|
|
$tickets = $query->latest('updated_at')
|
|
->paginate(25)
|
|
->withQueryString();
|
|
|
|
return Inertia::render('Admin/Tickets/Index', [
|
|
'tickets' => $tickets,
|
|
'filters' => [
|
|
'search' => $request->input('search', ''),
|
|
'status' => $request->input('status', ''),
|
|
'priority' => $request->input('priority', ''),
|
|
'department' => $request->input('department', ''),
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function show(SupportTicket $ticket): Response
|
|
{
|
|
$ticket->load([
|
|
'replies.user:id,name,email',
|
|
'user:id,name,email,status,company',
|
|
]);
|
|
|
|
return Inertia::render('Admin/Tickets/Show', [
|
|
'ticket' => $ticket,
|
|
]);
|
|
}
|
|
|
|
public function reply(Request $request, SupportTicket $ticket): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'body' => ['required', 'string', 'max:5000'],
|
|
'status' => ['nullable', 'in:open,in_progress,waiting,closed'],
|
|
]);
|
|
|
|
$reply = TicketReply::query()->create([
|
|
'ticket_id' => $ticket->id,
|
|
'user_id' => $request->user()->id,
|
|
'body' => $validated['body'],
|
|
'is_staff_reply' => true,
|
|
]);
|
|
|
|
$updateData = ['last_reply_at' => now()];
|
|
|
|
if (! empty($validated['status'])) {
|
|
$updateData['status'] = $validated['status'];
|
|
}
|
|
|
|
$ticket->update($updateData);
|
|
|
|
// Notify the customer about the staff reply
|
|
$ticket->user?->notify(new TicketStaffReplyNotification($ticket, $reply));
|
|
|
|
AuditLog::query()->create([
|
|
'user_id' => $ticket->user_id,
|
|
'admin_id' => $request->user()->id,
|
|
'action' => 'reply_ticket',
|
|
'resource_type' => 'support_ticket',
|
|
'resource_id' => $ticket->id,
|
|
'ip_address' => $request->ip(),
|
|
'user_agent' => $request->userAgent(),
|
|
]);
|
|
|
|
return redirect()->back()->with('success', 'Reply sent successfully.');
|
|
}
|
|
|
|
public function updateStatus(Request $request, SupportTicket $ticket): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'status' => ['required', 'in:open,in_progress,waiting,closed'],
|
|
]);
|
|
|
|
$oldStatus = $ticket->status;
|
|
|
|
$ticket->update(['status' => $validated['status']]);
|
|
|
|
// Notify the customer about the status change
|
|
$ticket->user?->notify(new TicketStatusChangedNotification($ticket, $oldStatus, $validated['status']));
|
|
|
|
AuditLog::query()->create([
|
|
'user_id' => $ticket->user_id,
|
|
'admin_id' => $request->user()->id,
|
|
'action' => 'update_ticket_status',
|
|
'resource_type' => 'support_ticket',
|
|
'resource_id' => $ticket->id,
|
|
'ip_address' => $request->ip(),
|
|
'user_agent' => $request->userAgent(),
|
|
'details' => json_encode(['status' => $validated['status']]),
|
|
]);
|
|
|
|
return redirect()->back()->with('success', 'Ticket status updated.');
|
|
}
|
|
}
|