Files
citrus-cms/app/Http/Controllers/NotificationController.php
T
2026-04-28 21:14:25 +03:00

969 lines
32 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Notification;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class NotificationController extends Controller
{
/**
* Base catalog of notification codes shown in the inbox navigation.
* Order matters for sidebar display.
*/
private const NOTIFICATION_CATALOG = [
'notification_deleted_joint_missing_comment' => '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;
}
}