'Deleted Joints Missing Comments', 'notification_repair_log_missing_new_joint' => 'Repair Logs Missing New Joint Number', 'notification_daily_repair_rate_high' => 'Daily Repair Rate Exceeds 12%', 'notification_line_list_missing_data' => 'Line List Missing Required Data', 'notification_wps_pdf_missing' => 'WPS PDF Document Missing', 'notification_naks_welder_cert_missing' => 'NAKS Welder Certificate Missing', 'notification_welding_equipment_pdf_missing' => 'Welding Equipment PDF Missing', 'notification_wpq_followup_pdf_missing' => 'WPQR PDF Missing', 'notification_weldlog_certificate_missing' => 'Certificate Number 1-2 Missing', 'notification_incoming_control_cert_missing' => 'Incoming Control Certificate Missing', 'notification_welder_id_card_created' => 'Welder ID Card Created', 'notification_document_revision_changed' => 'Document Revision Changed', 'notification_nde_matrix_not_in_weldlog' => 'NDE Matrix Not in Weldlog', 'notification_weldlog_not_in_line_list' => 'Weldlog Not in Line List', 'notification_line_list_ndt_missing' => 'Line List NDT Missing', 'notification_ndt_pmi_missing' => 'NDT PMI Test Missing', 'notification_ndt_fn_missing' => 'NDT FN Test Missing', 'notification_manage_ndt_unnecessary' => 'Unnecessary NDT Requests', 'notification_support_log_missing_in_weldlog' => 'Support Log Missing in Weldlog', 'notification_paint_system_incomplete' => 'Paint System Missing', 'notification_overall_repair_rate_high' => 'Overall Repair Rate Exceeds 5%', 'notification_weldlog_test_date_invalid' => 'Weldlog Test Date Before Welding Date', 'notification_nde_matrix_empty_row' => 'NDE Matrix Empty Row - NDT Assignment Required', 'notification_ndt_request_overdue' => 'NDT Request Overdue (10+ Days)', 'notification_test_log_pdf_missing_vt' => 'VT Test - PDF Report Missing', 'notification_test_log_pdf_missing_rt' => 'RT Test - PDF Report Missing', 'notification_test_log_pdf_missing_ut' => 'UT Test - PDF Report Missing', 'notification_test_log_pdf_missing_pt' => 'PT Test - PDF Report Missing', 'notification_test_log_pdf_missing_mt' => 'MT Test - PDF Report Missing', 'notification_test_log_pdf_missing_ht' => 'HT Test - PDF Report Missing', 'notification_test_log_pdf_missing_pmi' => 'PMI Test - PDF Report Missing', 'notification_test_log_pdf_missing_pwht' => 'PWHT Test - PDF Report Missing', 'notification_test_log_pdf_missing_ferrite' => 'Ferrite Test - PDF Report Missing', 'notification_feedback_report' => 'Feedback Report Notifications', 'notification_calibration_due_soon' => 'Calibration Due Soon', ]; /** * Get user's notifications with pagination * Performance: Uses indexes, pagination, and selective column loading * * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function getUserNotifications(Request $request) { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } $perPage = $request->input('per_page', 20); $onlyUnread = $request->input('only_unread', false); // Performance: Select only needed columns $query = Notification::select([ 'id', 'notification_code', 'title', 'message', 'link', 'is_read', 'filter_params', 'created_at' ]) ->forUser($user->id) ->orderBy('created_at', 'desc'); // Filter unread if requested if ($onlyUnread) { $query->unread(); } // Performance: Use pagination to limit results $notifications = $query->paginate($perPage); return response()->json($notifications); } /** * Get user's recent notifications (for dropdown) * Performance: Limited results, cached for 30 seconds * * @return \Illuminate\Http\JsonResponse */ public function getRecentNotifications() { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } // Cache key specific to user $cacheKey = "user_notifications_{$user->id}"; // Performance: Cache for 30 seconds to reduce database load $notifications = Cache::remember($cacheKey, 30, function () use ($user) { return Notification::select([ 'id', 'notification_code', 'title', 'message', 'link', 'is_read', 'filter_params', 'created_at' ]) ->forUser($user->id) ->recent(30) // Last 30 days ->orderBy('created_at', 'desc') ->limit(10) // Performance: Limit to 10 most recent ->get(); }); return response()->json([ 'notifications' => $notifications, 'unread_count' => $this->getUnreadCountValue($user->id) ]); } /** * Mark notification as read * Performance: Single update query * * @param int $id Notification ID * @return \Illuminate\Http\JsonResponse */ public function markAsRead($id) { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } // Performance: Check ownership and update in one query $updated = Notification::where('id', $id) ->where('user_id', $user->id) ->where('is_read', false) ->update(['is_read' => true]); if ($updated) { // Clear cache after marking as read Cache::forget("user_notifications_{$user->id}"); Cache::forget("user_unread_type_count_{$user->id}"); return response()->json([ 'success' => true, 'unread_count' => $this->getUnreadCountValue($user->id) ]); } return response()->json(['success' => false, 'message' => 'Notification not found or already read'], 404); } /** * Mark notification as unread * Performance: Single update query * * @param int $id Notification ID * @return \Illuminate\Http\JsonResponse */ public function markAsUnread($id) { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } // Performance: Check ownership and update in one query $updated = Notification::where('id', $id) ->where('user_id', $user->id) ->where('is_read', true) ->update(['is_read' => false]); if ($updated) { // Clear cache after marking as unread Cache::forget("user_notifications_{$user->id}"); Cache::forget("user_unread_type_count_{$user->id}"); return response()->json([ 'success' => true, 'unread_count' => $this->getUnreadCountValue($user->id) ]); } return response()->json(['success' => false, 'message' => 'Notification not found or already unread'], 404); } /** * Mark all notifications as read for user * Performance: Bulk update query * * @return \Illuminate\Http\JsonResponse */ public function markAllAsRead() { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } // Performance: Bulk update in single query $updated = Notification::where('user_id', $user->id) ->where('is_read', false) ->update(['is_read' => true]); // Clear cache Cache::forget("user_notifications_{$user->id}"); Cache::forget("user_unread_type_count_{$user->id}"); return response()->json([ 'success' => true, 'marked_count' => $updated, 'unread_count' => 0 ]); } /** * Get unread notification count * Performance: Cached count query * * @return \Illuminate\Http\JsonResponse */ public function getUnreadCount() { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } $count = $this->getUnreadCountValue($user->id); return response()->json(['unread_count' => $count]); } /** * Delete a notification * Performance: Single delete query with ownership check * * @param int $id Notification ID * @return \Illuminate\Http\JsonResponse */ public function delete($id) { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } // Performance: Delete with ownership check in one query $deleted = Notification::where('id', $id) ->where('user_id', $user->id) ->delete(); if ($deleted) { // Clear cache Cache::forget("user_notifications_{$user->id}"); Cache::forget("user_unread_type_count_{$user->id}"); return response()->json(['success' => true]); } return response()->json(['success' => false, 'message' => 'Notification not found'], 404); } /** * Delete all read notifications for user * Performance: Bulk delete query * * @return \Illuminate\Http\JsonResponse */ public function deleteReadNotifications() { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } // Performance: Bulk delete in single query $deletedCount = Notification::where('user_id', $user->id) ->where('is_read', true) ->delete(); // Clear cache Cache::forget("user_notifications_{$user->id}"); Cache::forget("user_unread_type_count_{$user->id}"); return response()->json([ 'success' => true, 'deleted_count' => $deletedCount ]); } /** * Show filtered list page for notification * * @param Request $request * @return \Illuminate\View\View */ public function showFilteredList(Request $request) { $user = Auth::user(); if (!$user) { abort(401, 'Unauthorized'); } $notificationId = $request->input('id'); if (!$notificationId) { abort(404, 'Notification ID is required'); } // Get notification and verify ownership $notification = Notification::where('id', $notificationId) ->where('user_id', $user->id) ->first(); if (!$notification) { abort(404, 'Notification not found'); } // Mark notification as read if not already read if (!$notification->is_read) { $notification->markAsRead(); Cache::forget("user_notifications_{$user->id}"); Cache::forget("user_unread_type_count_{$user->id}"); } return view('admin.type.notification-filtered-list', [ 'notification' => $notification ]); } /** * Get filtered data based on notification filter_params * * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function getFilteredData(Request $request) { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } $notificationId = $request->input('id'); if (!$notificationId) { return response()->json(['error' => 'Notification ID is required'], 400); } // Get notification and verify ownership $notification = Notification::where('id', $notificationId) ->where('user_id', $user->id) ->first(); if (!$notification) { return response()->json(['error' => 'Notification not found'], 404); } $filterParams = $notification->filter_params; if (empty($filterParams) || !isset($filterParams['table'])) { return response()->json(['error' => 'Filter parameters not found'], 400); } $tableName = $filterParams['table']; $conditions = $filterParams['conditions'] ?? []; $columnWhitelist = [ 'deleted_joints' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'type_of_joint', 'welding_date', 'spool_number', 'comment', 'deleted_date', 'updated_at' ], 'weld_logs' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'type_of_joint', 'welding_date', 'welder_1', 'welder_2', 'status', 'updated_at' ], 'repair_logs' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'new_joint_no', 'repair_status', 'repair_date', 'updated_at' ], 'test_packages' => [ 'id', 'package_no', 'package_title', 'status', 'hydro_test_date', 'pneumatic_test_date', 'updated_at' ], 'n_c_r_logs' => [ 'id', 'ncr_no', 'project', 'system', 'line_no', 'status', 'updated_at' ], 'r_f_i_s' => [ 'id', 'rfi_no', 'project', 'system', 'subject', 'status', 'updated_at' ], 'w_p_s' => [ 'id', 'pqr_no', 'technology_category', 'download', 'updated_at' ], 'naks_welders' => [ 'id', 'welder_name_en', 'welder_name_ru', 'welder_id', 'naks_certificate_no', 'updated_at' ], 'welding_equipment' => [ 'id', 'attestation', 'producer', 'download', 'updated_at' ], 'welder_tests' => [ 'id', 'name_surname', 'wpq_document_no', 'naks_id', 'download', 'updated_at' ], 'line_lists' => [ 'id', 'line_no', 'category', 'ndt', 'working_pressure_mpa', 'design_pressure_mpa', 'working_temperature', 'design_temperature', 'test_media', 'updated_at' ], 'supports' => [ 'id', 'support_code', 'materials', 'line_number', 'weld_or_assembled_date', 'updated_at' ], 'nde_matrices' => [ 'id', 'line', 'project', 'type_of_joint', 'updated_at' ], 'incoming_controls' => [ 'id', 'component_code', 'certificate_no', 'heat_number', 'updated_at' ], 'paint_systems' => [ 'id', 'paint_cycle', 'primer_coat_name_1', 'brand_name_1', 'thickness_1', 'updated_at' ], 'v_t_logs' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'vt_result', 'report_file', 'updated_at' ], 'radiographic_tests' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'rt_result', 'report_file', 'updated_at' ], 'ultrasonic_tests' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'ut_result', 'report_file', 'updated_at' ], 'p_t_logs' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'pt_result', 'report_file', 'updated_at' ], 'magnetic_tests' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'mt_result', 'report_file', 'updated_at' ], 'hardness_tests' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'ht_result', 'report_file', 'updated_at' ], 'p_m_i_tests' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'pmi_result', 'report_file', 'updated_at' ], 'p_w_h_t_s' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'pwht_result', 'report_file', 'updated_at' ], 'ferrits' => [ 'id', 'project', 'line_number', 'iso_number', 'no_of_the_joint_as_per_as_built_survey', 'ferrite_result', 'report_file', 'updated_at' ], 'test_pack_base_statuses' => [ 'id', 'test_package_no', 'drawing_no', 'area', 'discipline', 'subcontractor', 'updated_at' ], 'calibration_logs' => [ 'id', 'item_no', 'equipment_name', 'manufacturer', 'model_no', 'serial_no', 'quantity', 'calibration_date', 'calibration_due_date', 'status', 'certification_document', 'updated_at', ], ]; $selectedColumns = $columnWhitelist[$tableName] ?? ['*']; // Security: Only allow specific tables (whitelist) $allowedTables = [ 'deleted_joints', 'weld_logs', 'repair_logs', 'test_packages', 'n_c_r_logs', 'r_f_i_s', 'w_p_s', 'naks_welders', 'welding_equipment', 'welder_tests', 'line_lists', 'supports', 'nde_matrices', 'incoming_controls', 'paint_systems', 'v_t_logs', 'radiographic_tests', 'ultrasonic_tests', 'p_t_logs', 'magnetic_tests', 'hardness_tests', 'p_m_i_tests', 'p_w_h_t_s', 'ferrits', 'test_pack_base_statuses', 'calibration_logs', ]; if (!in_array($tableName, $allowedTables)) { return response()->json(['error' => 'Table not allowed'], 403); } try { // Build query based on filter_params $query = DB::table($tableName)->select($selectedColumns); // Apply conditions if (!empty($conditions)) { $hasOrConditions = false; $orConditions = []; foreach ($conditions as $key => $value) { if ($key === 'or') { // Collect OR conditions to process later $hasOrConditions = true; $orConditions = is_array($value) ? $value : []; } else { // Handle normal WHERE conditions $this->applyColumnFilter($query, $key, $value); } } // Handle OR conditions (applied after normal conditions) if ($hasOrConditions && !empty($orConditions)) { $query->where(function($q) use ($orConditions) { $isFirst = true; foreach ($orConditions as $orCondition) { if (!is_array($orCondition)) { continue; } foreach ($orCondition as $orKey => $orValue) { $this->applyColumnFilter($q, $orKey, $orValue, !$isFirst); $isFirst = false; } } }); } } $maxRows = max(50, min((int) $request->input('max_rows', 1200), 5000)); $countQuery = clone $query; $totalRows = $countQuery->count(); $data = $query->orderBy('id', 'desc')->limit($maxRows)->get(); $columns = $selectedColumns === ['*'] ? (!empty($data) ? array_keys((array) $data->first()) : []) : $selectedColumns; return response()->json([ 'success' => true, 'data' => $data, 'tableName' => $tableName, 'columns' => $columns, 'count' => $totalRows, 'returned_count' => count($data), 'has_more' => $totalRows > $maxRows ]); } catch (\Exception $e) { Log::error('Filtered data error: ' . $e->getMessage()); return response()->json(['error' => 'Error fetching filtered data: ' . $e->getMessage()], 500); } } /** * Get notification types with counts for inbox sidebar * * @return \Illuminate\Http\JsonResponse */ public function getNotificationTypes() { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } $counts = Notification::select( 'notification_code', DB::raw('count(*) as total'), DB::raw('sum(case when is_read = 0 then 1 else 0 end) as unread') ) ->where('user_id', $user->id) ->groupBy('notification_code') ->get() ->keyBy('notification_code'); $catalog = collect(self::NOTIFICATION_CATALOG); $types = $catalog->map(function ($title, $code) use ($counts) { $stat = $counts->get($code); return [ 'code' => $code, 'title' => $title, 'total' => $stat ? (int) $stat->total : 0, 'unread' => $stat ? (int) $stat->unread : 0, ]; }) ->values(); // Append any notification codes not defined in the catalog to avoid hiding new types $extraTypes = $counts->reject(function ($value, $code) use ($catalog) { return $catalog->has($code); }) ->map(function ($stat, $code) { return [ 'code' => $code, 'title' => ucwords(str_replace('_', ' ', $code)), 'total' => (int) $stat->total, 'unread' => (int) $stat->unread, ]; }) ->values(); if ($extraTypes->isNotEmpty()) { $types = $types->merge($extraTypes); } // Get all/unread counts $allCount = Notification::where('user_id', $user->id)->count(); $unreadCount = Notification::where('user_id', $user->id)->where('is_read', false)->count(); return response()->json([ 'types' => $types, 'all' => [ 'total' => $allCount, 'unread' => $unreadCount ] ]); } /** * Get notifications by type * * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function getNotificationsByType(Request $request) { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } $type = $request->input('type', 'all'); $perPage = $request->input('per_page', 100); $query = Notification::select([ 'id', 'notification_code', 'title', 'message', 'link', 'is_read', 'filter_params', 'created_at' ]) ->where('user_id', $user->id); switch ($type) { case 'unread': $query->where('is_read', false); break; case 'batch': $query->whereNotNull('filter_params'); break; case 'single': $query->whereNull('filter_params'); break; default: if ($type !== 'all') { $query->where('notification_code', $type); } break; } $notifications = $query->orderBy('created_at', 'desc') ->get(); // Remove duplicates based on message, link, and normalized filter_params $seen = []; $uniqueNotifications = []; foreach ($notifications as $notification) { // Normalize filter_params for comparison $filterParams = normalizeFilterParams($notification->filter_params); // Create unique key $key = $notification->message . '|' . ($notification->link ?? '') . '|' . $filterParams; // Keep only the first (most recent) occurrence if (!isset($seen[$key])) { $seen[$key] = true; $uniqueNotifications[] = $notification; } } // Convert to paginated response $currentPage = $request->input('page', 1); $perPage = (int) $perPage; $offset = ($currentPage - 1) * $perPage; $paginatedItems = array_slice($uniqueNotifications, $offset, $perPage); $paginated = new LengthAwarePaginator( $paginatedItems, count($uniqueNotifications), $perPage, $currentPage, ['path' => $request->url(), 'query' => $request->query()] ); return response()->json($paginated); } /** * Helper: Get cached unread count * Performance: Cached for 1 minute * * @param int $userId * @return int */ private function getUnreadCountValue($userId) { $cacheKey = "user_unread_type_count_{$userId}"; // Performance: Cache count for 1 minute // Return count of distinct notification types with unread notifications (not total notification count) return Cache::remember($cacheKey, 60, function () use ($userId) { return DB::table('notifications') ->where('user_id', $userId) ->where('is_read', false) ->select(DB::raw('COUNT(DISTINCT notification_code) as count')) ->value('count') ?? 0; }); } /** * Apply a single column filter to the query, supporting scalar equality and list (IN) filters. */ private function applyColumnFilter($query, string $column, $value, bool $useOr = false): void { if (is_array($value) && $this->isScalarList($value)) { $list = array_values(array_unique(array_filter($value, function ($item) { return $item !== null && $item !== ''; }))); if (empty($list)) { $useOr ? $query->orWhereRaw('1 = 0') : $query->whereRaw('1 = 0'); return; } $useOr ? $query->orWhereIn($column, $list) : $query->whereIn($column, $list); return; } if ($value === null) { $useOr ? $query->orWhereNull($column) : $query->whereNull($column); } elseif ($value === '') { $useOr ? $query->orWhere($column, '') : $query->where($column, ''); } else { $useOr ? $query->orWhere($column, $value) : $query->where($column, $value); } } /** * Determine if the given array is a simple list of scalar values (suitable for whereIn). */ private function isScalarList(array $value): bool { if (array_keys($value) !== range(0, count($value) - 1)) { return false; } foreach ($value as $item) { if (is_array($item) || is_object($item)) { return false; } } return true; } }