$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()); } } }