Files
website/website/app/Http/Controllers/Admin/TicketController.php
Claude Dev 015949a7f6 Add email integration for ticket system and IMAP inbound support
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>
2026-02-09 17:10:04 -05:00

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.');
}
}