Files
portal-api/app/Http/Controllers/Api/Client/Ticket/TicketController.php
T
2026-09-28 08:26:29 +07:00

235 lines
7.5 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Client\Ticket;
use App\Http\Controllers\Controller;
use App\Http\Resources\Api\Ticket\TicketResource;
use App\Http\Resources\Api\Ticket\TicketReplyResource;
use App\Jobs\Ticket\TicketJobs;
use App\Mail\TicketCreated;
use App\Mail\Admin\TicketAdminNotification;
use App\Models\Setting;
use App\Models\Ticket;
use App\Models\Notification;
use App\Events\TicketReplyCreated;
use App\Repositories\Ticket\TicketRepositoryInterface;
use App\Repositories\TicketReply\TicketReplyRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class TicketController extends Controller
{
public function __construct(
protected TicketRepositoryInterface $ticketRepository,
protected TicketReplyRepositoryInterface $ticketReplyRepository,
) {}
/**
* List tickets for the authenticated customer (paginated, filterable by status).
*/
public function index(Request $request)
{
$customer = auth('client')->user();
$query = Ticket::query()
->where('customer_id', $customer->id)
->latest('id');
if ($request->filled('status')) {
$query->where('status', $request->input('status'));
}
$paginated = $query->paginate(15);
return response()->json([
'error' => false,
'data' => [
'items' => TicketResource::collection($paginated->items()),
'total' => $paginated->total(),
'per_page' => $paginated->perPage(),
'current_page' => $paginated->currentPage(),
'last_page' => $paginated->lastPage(),
],
]);
}
/**
* Create a new ticket for the authenticated customer.
*/
public function store(Request $request)
{
$request->validate([
'subject' => ['required', 'string', 'min:5', 'max:200'],
'description' => ['required', 'string', 'min:20'],
'room' => ['nullable', 'string'],
'data' => ['nullable', 'array'],
'images' => ['nullable', 'array', 'max:5'],
'images.*' => ['image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
]);
$customer = auth('client')->user();
$ticketNum = 'TKT-' . now()->format('Ymd') . '-' . strtoupper(Str::random(5));
$gallery = [];
foreach ($request->file('images', []) as $image) {
$path = $image->store('tickets', 'public');
$gallery[] = Storage::disk('public')->url($path);
}
$ticket = $this->ticketRepository->create([
'uuid' => (string) Str::uuid(),
'ticket_num' => $ticketNum,
'subject' => $request->input('subject'),
'description' => $request->input('description'),
'status' => 'open',
'room' => $request->input('room'),
'data' => $request->input('data') ? json_encode($request->input('data')) : null,
'gallery' => $gallery ?: null,
'customer_id' => $customer->id,
]);
TicketJobs::dispatch($ticket, $customer);
Mail::to($customer->email)->queue(new TicketCreated($ticket, $customer));
$setting = Setting::where('key', 'setting')->first();
$adminEmail = $setting ? (json_decode((string) $setting->value)?->admin_email ?? null) : null;
if ($adminEmail) {
Mail::to($adminEmail)->queue(new TicketAdminNotification($ticket, $customer));
}
return response()->json([
'error' => false,
'message' => 'Ticket đã được khởi tạo.',
'data' => new TicketResource($ticket),
], 201);
}
/**
* Show a single ticket with its replies (must belong to customer).
*/
public function show(string $ticket_num)
{
$customer = auth('client')->user();
$ticket = Ticket::query()
->with(['reply.user', 'customer'])
->where('ticket_num', $ticket_num)
->where('customer_id', $customer->id)
->first();
if (!$ticket) {
return response()->json([
'error' => true,
'message' => 'Ticket không tồn tại hoặc bạn không có quyền truy cập.',
], 404);
}
return response()->json([
'error' => false,
'data' => new TicketResource($ticket),
]);
}
/**
* Add a reply to a ticket (customer side).
*/
public function reply(Request $request, string $ticket_num)
{
$request->validate([
'message' => ['required', 'string'],
]);
$customer = auth('client')->user();
$ticket = Ticket::query()
->where('ticket_num', $ticket_num)
->where('customer_id', $customer->id)
->first();
if (!$ticket) {
return response()->json([
'error' => true,
'message' => 'Ticket không tồn tại hoặc bạn không có quyền truy cập.',
], 404);
}
if (in_array($ticket->status, ['closed', 'resolved'])) {
return response()->json([
'error' => true,
'message' => 'Ticket đã đóng, không thể thêm phản hồi.',
], 422);
}
$reply = $this->ticketReplyRepository->create([
'ticket_id' => $ticket->id,
'customer_id' => $customer->id,
'message' => $request->input('message'),
'user_id' => null,
]);
Notification::notifyAdmins([
'type' => 'ticket_reply',
'title' => 'Có phản hồi mới từ khách hàng',
'body' => $ticket->ticket_num . ' - ' . $ticket->subject,
'link' => '/tickets/' . $ticket->ticket_num,
'data' => [
'ticket_num' => $ticket->ticket_num,
'reply_id' => $reply->id,
],
]);
broadcast(new TicketReplyCreated(
ticketNum: $ticket->ticket_num,
customerId: $customer->id,
replyId: $reply->id,
));
// Update ticket status back to open (customer replied)
$ticket->update(['status' => 'open']);
return response()->json([
'error' => false,
'message' => 'Phản hồi đã được gửi.',
'data' => new TicketReplyResource($reply),
], 201);
}
/**
* Customer closes their ticket.
*/
public function close(string $ticket_num)
{
$customer = auth('client')->user();
$ticket = Ticket::query()
->where('ticket_num', $ticket_num)
->where('customer_id', $customer->id)
->first();
if (!$ticket) {
return response()->json([
'error' => true,
'message' => 'Ticket không tồn tại hoặc bạn không có quyền truy cập.',
], 404);
}
if ($ticket->status === 'closed') {
return response()->json([
'error' => true,
'message' => 'Ticket đã được đóng trước đó.',
], 422);
}
$ticket->update(['status' => 'closed']);
return response()->json([
'error' => false,
'message' => 'Ticket đã được đóng.',
'data' => new TicketResource($ticket->fresh()),
]);
}
}