b05e074456
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local> Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local> Reviewed-on: #11
339 lines
11 KiB
PHP
339 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Modules\Feedback\Http\Controllers;
|
|
|
|
use App\Http\Controllers\BaseCrudController;
|
|
use App\Models\Document;
|
|
use Exception;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Modules\Feedback\Entities\Feedback;
|
|
use Modules\Feedback\Http\Requests\FeedbackRequest;
|
|
use Modules\Feedback\Repositories\Contracts\FeedbackRepositoryInterface;
|
|
use Modules\Feedback\Transformers\FeedbackResource;
|
|
use Modules\Feedback\Notifications\FeedbackNotification;
|
|
use Modules\Auth\Entities\User;
|
|
use Laravel\Sanctum\PersonalAccessToken;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
|
|
class FeedbackController extends BaseCrudController
|
|
{
|
|
protected $modelClass = Feedback::class;
|
|
protected $resourceClass = FeedbackResource::class;
|
|
protected $requestClass = FeedbackRequest::class;
|
|
protected $resourceName = 'maklum balas';
|
|
protected $resourceNamePlural = 'maklum balas';
|
|
|
|
public function __construct(FeedbackRepositoryInterface $repository)
|
|
{
|
|
parent::__construct($repository);
|
|
}
|
|
|
|
/**
|
|
* Override getIndexData to handle filters
|
|
*/
|
|
protected function getIndexData(Request $request, int $perPage = 10, string $search = '', string $sortBy = 'id', string $sortOrder = 'asc')
|
|
{
|
|
// Extract filter parameters from request
|
|
$filters = [
|
|
'type' => $request->get('type'),
|
|
'status' => $request->get('status'),
|
|
'priority' => $request->get('priority'),
|
|
];
|
|
|
|
// Always try paginated methods first when perPage is specified
|
|
if ($perPage > 0) {
|
|
if (method_exists($this->repository, 'getAllWithRelationsPaginated')) {
|
|
return $this->repository->getAllWithRelationsPaginated($perPage, $search, $sortBy, $sortOrder, $filters);
|
|
}
|
|
|
|
if (method_exists($this->repository, 'getAllPaginated')) {
|
|
return $this->repository->getAllPaginated($perPage, $search, $sortBy, $sortOrder, $filters);
|
|
}
|
|
}
|
|
|
|
// Fallback to non-paginated methods
|
|
if (method_exists($this->repository, 'getAllWithRelations')) {
|
|
return $this->repository->getAllWithRelations($search, $sortBy, $sortOrder);
|
|
}
|
|
|
|
return $this->repository->all($search, $sortBy, $sortOrder);
|
|
}
|
|
|
|
/**
|
|
* Store a newly created resource in storage (public access)
|
|
*/
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
// Skip authorization for public feedback submission
|
|
try {
|
|
$validated = $this->validateRequest($request);
|
|
$data = $this->prepareStoreData($validated);
|
|
|
|
$item = $this->repository->create($data);
|
|
|
|
$this->uploadAttachments($item, $request);
|
|
|
|
// Load relationships
|
|
$item->load(['user', 'documents']);
|
|
|
|
// Send notification to admins
|
|
$this->notifyAdminsForNewFeedback($item);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => new $this->resourceClass($item),
|
|
'message' => 'Maklum balas berjaya dihantar. Terima kasih!',
|
|
], 201);
|
|
|
|
} catch (Exception $e) {
|
|
Log::error("Error creating {$this->resourceName}: ".$e->getMessage());
|
|
|
|
return $this->errorResponse($this->getErrorMessage('store').': '.$e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare data for store method
|
|
*/
|
|
protected function prepareStoreData(array $validated): array
|
|
{
|
|
$data = $validated;
|
|
|
|
// Check if user is authenticated by looking for the Authorization header
|
|
$authHeader = request()->header('Authorization');
|
|
if ($authHeader && str_starts_with($authHeader, 'Bearer ')) {
|
|
// Try to authenticate the user manually
|
|
$token = str_replace('Bearer ', '', $authHeader);
|
|
$personalAccessToken = PersonalAccessToken::findToken($token);
|
|
|
|
if ($personalAccessToken) {
|
|
$data['user_id'] = $personalAccessToken->tokenable_id;
|
|
} else {
|
|
$data['user_id'] = null;
|
|
}
|
|
} else {
|
|
// User is not authenticated
|
|
$data['user_id'] = null;
|
|
}
|
|
|
|
$data['browser_info'] = $this->getBrowserInfo(request());
|
|
|
|
unset($data['images'], $data['videos']);
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Upload image and video attachments via HasDocuments.
|
|
*/
|
|
protected function uploadAttachments(Feedback $feedback, Request $request): void
|
|
{
|
|
if ($request->hasFile('images')) {
|
|
foreach ($request->file('images') as $image) {
|
|
$feedback->uploadDocument($image, Feedback::IMAGE_DOCUMENT_TYPE);
|
|
}
|
|
}
|
|
|
|
if ($request->hasFile('videos')) {
|
|
foreach ($request->file('videos') as $video) {
|
|
$feedback->uploadDocument($video, Feedback::VIDEO_DOCUMENT_TYPE);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare data for update method
|
|
*/
|
|
protected function prepareUpdateData(array $validated, $feedback): array
|
|
{
|
|
$data = $validated;
|
|
|
|
unset($data['images'], $data['videos']);
|
|
|
|
// Set resolved_at timestamp when status changes to resolved
|
|
if (isset($data['status']) && $data['status'] === 'resolved') {
|
|
$data['resolved_at'] = now();
|
|
} elseif (isset($data['status']) && $data['status'] !== 'resolved') {
|
|
$data['resolved_at'] = null;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Update the specified resource in storage.
|
|
*/
|
|
public function update(Request $request, string $id): JsonResponse
|
|
{
|
|
$this->authorize('update', $this->modelClass);
|
|
|
|
try {
|
|
$item = $this->repository->findById($id);
|
|
|
|
if (! $item) {
|
|
return $this->errorResponse($this->getNotFoundMessage(), 404);
|
|
}
|
|
|
|
$validated = $this->validateRequest($request);
|
|
$data = $this->prepareUpdateData($validated, $item);
|
|
|
|
$item->update($data);
|
|
$item->load(['user', 'assignedUser', 'documents']);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => new $this->resourceClass($item),
|
|
'message' => $this->getSuccessMessage('update'),
|
|
]);
|
|
} catch (Exception $e) {
|
|
Log::error("Error updating {$this->resourceName}: ".$e->getMessage());
|
|
|
|
return $this->errorResponse($this->getErrorMessage('update').': '.$e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load relations for show method
|
|
*/
|
|
protected function loadShowRelations($item)
|
|
{
|
|
return $item->load(['user', 'assignedUser', 'documents']);
|
|
}
|
|
|
|
/**
|
|
* Check dependencies before deletion
|
|
*/
|
|
protected function checkDependencies($feedback): ?\Illuminate\Http\JsonResponse
|
|
{
|
|
foreach ($feedback->documents as $document) {
|
|
$feedback->deleteDocument($document->id);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Stream a feedback attachment inline (image/video preview).
|
|
*/
|
|
public function serveDocument(string $id, string $documentId): BinaryFileResponse
|
|
{
|
|
$this->authorize('view', $this->modelClass);
|
|
|
|
$feedback = Feedback::findOrFail($id);
|
|
$document = $feedback->documents()->findOrFail($documentId);
|
|
|
|
$disk = Storage::disk(Document::STORAGE_DISK);
|
|
|
|
if (! $disk->exists($document->path)) {
|
|
abort(404, 'File not found');
|
|
}
|
|
|
|
return response()->file($disk->path($document->path), [
|
|
'Content-Type' => $document->mime_type ?? 'application/octet-stream',
|
|
'Content-Disposition' => 'inline; filename="'.$document->name.'"',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get feedback statistics
|
|
*/
|
|
public function statistics(): JsonResponse
|
|
{
|
|
$stats = [
|
|
'total' => Feedback::count(),
|
|
'open' => Feedback::open()->count(),
|
|
'resolved' => Feedback::resolved()->count(),
|
|
'by_type' => Feedback::selectRaw('type, COUNT(*) as count')
|
|
->groupBy('type')
|
|
->pluck('count', 'type'),
|
|
'by_priority' => Feedback::selectRaw('priority, COUNT(*) as count')
|
|
->groupBy('priority')
|
|
->pluck('count', 'priority'),
|
|
'by_status' => Feedback::selectRaw('status, COUNT(*) as count')
|
|
->groupBy('status')
|
|
->pluck('count', 'status'),
|
|
];
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $stats
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get user's own feedback
|
|
*/
|
|
public function myFeedback(Request $request): JsonResponse
|
|
{
|
|
$query = Feedback::where('user_id', auth()->id())
|
|
->with(['assignedUser', 'documents'])
|
|
->orderBy('created_at', 'desc');
|
|
|
|
// Apply filters
|
|
if ($request->has('status') && $request->status) {
|
|
$query->ofStatus($request->status);
|
|
}
|
|
|
|
if ($request->has('type') && $request->type) {
|
|
$query->ofType($request->type);
|
|
}
|
|
|
|
$perPage = $request->get('per_page', 15);
|
|
$feedback = $query->paginate($perPage);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => FeedbackResource::collection($feedback->items()),
|
|
'meta' => [
|
|
'current_page' => $feedback->currentPage(),
|
|
'last_page' => $feedback->lastPage(),
|
|
'per_page' => $feedback->perPage(),
|
|
'total' => $feedback->total(),
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get browser information from request
|
|
*/
|
|
private function getBrowserInfo(Request $request): array
|
|
{
|
|
return [
|
|
'user_agent' => $request->userAgent(),
|
|
'ip_address' => $request->ip(),
|
|
'referer' => $request->header('referer'),
|
|
'accept_language' => $request->header('accept-language'),
|
|
'screen_resolution' => $request->input('screen_resolution'),
|
|
'viewport_size' => $request->input('viewport_size'),
|
|
'timezone' => $request->input('timezone'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Notify admins about new feedback submission
|
|
*/
|
|
private function notifyAdminsForNewFeedback(Feedback $feedback): void
|
|
{
|
|
try {
|
|
$adminRoles = ['PENTADBIR', 'PS 2 KJC', 'PS 2 ALAT'];
|
|
|
|
// Get users with specific roles plus admins (PENTADBIR and DEVELOPER)
|
|
$adminUsers = $this->getUsersWithRolesAndAdmins($adminRoles);
|
|
|
|
$sender = auth()->user() ?? $feedback->user; // Use current user as sender, or feedback submitter if no auth
|
|
|
|
foreach ($adminUsers as $admin) {
|
|
try {
|
|
$admin->notify(new FeedbackNotification($feedback, $sender));
|
|
} catch (Exception $e) {
|
|
Log::error('Failed to send feedback notification: '.$e->getMessage());
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::error('Failed to notify admins for new feedback: '.$e->getMessage());
|
|
}
|
|
}
|
|
} |