/monsoonyatraactivitylist/report?token=TOKEN */ class MonsoonyatraactivitylistController extends Controller { private const ALLOWED_DOMAINS = ['titans', 'team2']; private const ACTIVITIES_TABLE = 'team3_monsoon_yatra_activities'; private const IMPORTS_TABLE = 'team3_monsoon_yatra_imports'; private const SESSION_TOKEN_KEY = 'monsoon_yatra_report_token'; private ?string $accessToken = null; private ?object $reportUser = null; public function __construct() { $this->_viewname = 'monsoonyatraactivitylist'; $this->_title = 'Monsoon Yatra Activity List'; $this->_base_url = '/monsoonyatraactivitylist'; $this->middleware(function ($request, $next) { $action = $request->route('action'); if (in_array($action, ['report', 'export'], true)) { return $next($request); } if ($action === null && $request->route()->getName() === 'monsoonyatraactivitylist-list' && $request->filled('token')) { return $next($request); } if (!Auth::check()) { return redirect('/login'); } $this->assertAllowedWebAccess(); return $next($request); }); } public function list(Request $request) { $this->bootstrapTokenSession($request); if ($request->hasSession()) { $request->session()->save(); } if ($request->filled('token')) { return redirect($this->buildUrl('/report', $request)); } return redirect($this->_base_url . '/report'); } public function report(Request $request) { $this->bootstrapTokenSession($request); if ($request->hasSession()) { $request->session()->save(); } $token = $this->assertReportAccess($request); $filters = $this->resolveFilters($request); $query = $this->buildFilteredQuery($filters); $perPage = $this->resolvePerPage($request); $paginationAppends = $filters; if ($token !== '') { $paginationAppends['token'] = $token; } $rows = (clone $query) ->orderBy('zone') ->orderBy('state') ->orderBy('activity_date') ->orderBy('educator_name') ->orderBy('doctor_name') ->paginate($perPage) ->appends($paginationAppends); return $this->view('report')->with([ 'title' => $this->_title, 'base_url' => $this->_base_url, 'token' => $token, 'rows' => $rows, 'filters' => $filters, 'filterOptions' => $this->filterOptions(), 'canImport' => $this->userCanImport(), 'buttons' => $this->getReportButtons(), ]); } public function export(Request $request) { $this->bootstrapTokenSession($request); $this->assertReportAccess($request); $filters = $this->resolveFilters($request); $rows = $this->buildFilteredQuery($filters) ->orderBy('zone') ->orderBy('state') ->orderBy('activity_date') ->orderBy('educator_name') ->orderBy('doctor_name') ->get(); $header = [ 'Date', 'State', 'Zone', 'Educator HQ', 'Educator Name', 'Educator Manager', 'Camp Area', 'Time', 'MSL Code', 'Doctor Name', 'Doctor Speciality', 'Chemist Name', ]; $excelData = [$header]; foreach ($rows as $row) { $excelData[] = [ $this->formatDisplayDate($row->activity_date), $row->state, $row->zone, $row->educator_hq, $row->educator_name, $row->educator_manager, $row->camp_area, $row->activity_time, $row->msl_code, $row->doctor_name, $row->doctor_speciality, $row->chemist_name, ]; } return Excel::download(new InvoicesExport($excelData), 'monsoon-yatra-activity-list.xlsx'); } public function import(Request $request) { $this->assertAllowedAdmin(); $imports = DB::table(self::IMPORTS_TABLE) ->orderByDesc('created_at') ->limit(100) ->get(); return $this->view('import')->with([ 'title' => 'Import Monsoon Yatra Activity Data', 'base_url' => $this->_base_url, 'imports' => $imports, ]); } public function postimport(Request $request) { $this->assertAllowedAdmin(); try { $validator = Validator::make($request->all(), [ 'import_file' => 'required|file|max:10240', ], [ 'import_file.required' => 'Please choose a file to upload.', 'import_file.file' => 'The upload did not contain a valid file.', 'import_file.max' => 'The file may not be larger than 10MB.', ]); if ($validator->fails()) { return redirect()->back() ->withErrors($validator) ->withInput() ->with('error', $validator->errors()->first()); } $file = $request->file('import_file'); $extension = strtolower((string) $file->getClientOriginalExtension()); if (!in_array($extension, ['xlsx', 'xls', 'csv'], true)) { return redirect()->back() ->withInput() ->with('error', 'Only .xlsx, .xls, and .csv files are supported.'); } $importId = DB::table(self::IMPORTS_TABLE)->insertGetId([ 'file_name' => $file->getClientOriginalName(), 'status' => 'processing', 'uploaded_by' => (string) Auth::id(), 'created_at' => now(), 'updated_at' => now(), ]); $inserted = 0; $replaced = 0; $skipped = 0; $errors = []; $rows = $this->parseImportFile($file); if (empty($rows['headers']) || empty($rows['data'])) { throw new \RuntimeException('The file is empty or has no data rows.'); } $columnIndex = $this->mapHeaderIndexes($rows['headers']); if (!isset($columnIndex['activity_date'])) { throw new \RuntimeException('Required column Date was not found in the file header row.'); } foreach ($rows['data'] as $rowIndex => $row) { $lineNo = $rowIndex + 2; try { $record = $this->mapRowToRecord($row, $columnIndex); if (empty($record['activity_date'])) { $skipped++; continue; } if (empty($record['msl_code']) && empty($record['doctor_name'])) { $skipped++; continue; } $existing = DB::table(self::ACTIVITIES_TABLE) ->where('activity_date', $record['activity_date']) ->where('msl_code', $record['msl_code'] ?? '') ->where('educator_name', $record['educator_name'] ?? '') ->where('activity_time', $record['activity_time'] ?? '') ->first(); $payload = array_merge($record, [ 'import_id' => $importId, 'updated_at' => now(), ]); if ($existing) { DB::table(self::ACTIVITIES_TABLE)->where('id', $existing->id)->update($payload); $replaced++; } else { DB::table(self::ACTIVITIES_TABLE)->insert(array_merge($payload, [ 'created_at' => now(), ])); $inserted++; } } catch (\Throwable $e) { $errors[] = $this->formatRowError($lineNo, $e); } } $status = empty($errors) ? 'completed' : 'completed_with_errors'; $this->updateImportLog($importId, [ 'status' => $status, 'total_rows' => count($rows['data']), 'inserted_count' => $inserted, 'replaced_count' => $replaced, 'skipped_count' => $skipped, 'error_count' => count($errors), 'error_log' => empty($errors) ? null : implode("\n", array_slice($errors, 0, 50)), 'processed_at' => now(), 'updated_at' => now(), ]); $message = "Import complete. Inserted: {$inserted}, Replaced: {$replaced}, Skipped: {$skipped}."; if (!empty($errors)) { $message .= ' Errors: ' . count($errors) . ' (see import log).'; } return redirect($this->_base_url . '/import')->with('message', $message); } catch (\Throwable $e) { Log::error('Monsoon Yatra activity import failed', [ 'error' => $e->getMessage(), 'user_id' => Auth::id(), ]); if (isset($importId)) { $this->updateImportLog($importId, [ 'status' => 'failed', 'error_log' => $e->getMessage(), 'processed_at' => now(), 'updated_at' => now(), ]); } return redirect()->back() ->withInput() ->with('error', $this->sanitizeDbString($e->getMessage()) ?: 'Import failed.'); } } protected function getReportButtons(): array { $buttons = []; if (Auth::check() && $this->userCanImport()) { $buttons[] = ['title' => 'Import', 'link' => $this->_base_url . '/import']; } return $buttons; } protected function userCanImport(): bool { $permissions = $this->resolveUserPermissions(); return in_array('all_data_access', $permissions, true) || in_array('formexports_get_all', $permissions, true); } protected function resolveUserPermissions(): array { if ($this->reportUser !== null) { $permissions = $this->reportUser->permissions ?? []; if (is_array($permissions)) { return $permissions; } if (($this->reportUser->all_data_access ?? false) === true) { return ['all_data_access']; } return []; } $user = Auth::user(); if ($user === null) { return []; } $permissions = json_decode($user->permissions ?? '[]', true); return is_array($permissions) ? $permissions : []; } protected function assertAllowedWebAccess(): void { $user = Auth::user(); if (!$this->userDomainIsAllowed((string) ($user->domain ?? ''))) { abort(403); } } protected function assertAllowedAdmin(): void { $this->assertAllowedWebAccess(); if (!$this->userCanImport()) { abort(403); } } protected function assertReportAccess(Request $request): string { $token = $this->resolveAccessToken($request); if ($token) { $payload = $this->decodeTokenPayload($token); $user = $this->getUserFromToken($token); if (!$user || !$this->isAllowedDomainUser($user)) { abort(403, 'Access denied. A valid titans or team2 token is required.'); } if ($this->isTokenPayloadExpired($payload)) { $this->forgetPersistedToken($request); abort(401, 'Your access token has expired. Please open a fresh link from the app.'); } $this->accessToken = $token; $this->reportUser = $user; return $token; } if (Auth::check() && $this->userDomainIsAllowed((string) (Auth::user()->domain ?? ''))) { return ''; } if ($request->filled('token')) { abort(403, 'Invalid or truncated access token. Please open the full link from the app.'); } abort(401, 'Access token is required. Open this report with ?token=... from the app.'); } protected function bootstrapTokenSession(Request $request): void { $token = $this->extractRequestToken($request); if ($token === '') { return; } $payload = $this->decodeTokenPayload($token); $user = is_object($payload) ? ($payload->user ?? null) : null; if ($user && $this->isAllowedDomainUser($user) && !$this->isTokenPayloadExpired($payload)) { $this->persistAccessToken($request, $token); } } protected function resolveAccessToken(Request $request): ?string { $fromRequest = $this->extractRequestToken($request); if ($fromRequest !== '') { $payload = $this->decodeTokenPayload($fromRequest); $user = is_object($payload) ? ($payload->user ?? null) : null; if ($user && $this->isAllowedDomainUser($user) && !$this->isTokenPayloadExpired($payload)) { $this->persistAccessToken($request, $fromRequest); return $fromRequest; } } $storedToken = $this->loadPersistedToken($request); if ($storedToken !== '') { $payload = $this->decodeTokenPayload($storedToken); $user = is_object($payload) ? ($payload->user ?? null) : null; if ($user && $this->isAllowedDomainUser($user) && !$this->isTokenPayloadExpired($payload)) { return $storedToken; } $this->forgetPersistedToken($request); } return null; } protected function extractRequestToken(Request $request): string { $raw = $request->query('token', $request->input('token')); if ($raw === null || $raw === '') { $raw = $request->header('Authorization') ?? ''; } return $this->normalizeRawToken((string) $raw); } protected function persistAccessToken(Request $request, string $token): void { $request->session()->put(self::SESSION_TOKEN_KEY, $token); $this->tokenStorage()->put($this->tokenStoragePath($request), $token); } protected function loadPersistedToken(Request $request): string { if ($request->session()->has(self::SESSION_TOKEN_KEY)) { $sessionToken = $this->normalizeRawToken((string) $request->session()->get(self::SESSION_TOKEN_KEY)); if ($sessionToken !== '') { return $sessionToken; } } $path = $this->tokenStoragePath($request); if ($this->tokenStorage()->exists($path)) { return $this->normalizeRawToken((string) $this->tokenStorage()->get($path)); } return ''; } protected function forgetPersistedToken(Request $request): void { $request->session()->forget(self::SESSION_TOKEN_KEY); $path = $this->tokenStoragePath($request); if ($this->tokenStorage()->exists($path)) { $this->tokenStorage()->delete($path); } } protected function tokenStoragePath(Request $request): string { return 'monsoon_yatra_report_tokens/' . hash('sha256', $request->session()->getId()); } protected function tokenStorage() { return Storage::disk('local'); } protected function isTokenPayloadExpired(?object $payload): bool { if (!is_object($payload) || !isset($payload->exp)) { return false; } return (int) $payload->exp < time(); } protected function normalizeRawToken(string $token): string { $token = trim($token); if (stripos($token, 'Bearer ') === 0) { $token = trim(substr($token, 7)); } if (substr_count($token, '.') === 2) { $parts = explode('.', $token); $parts[1] = str_replace(' ', '+', $parts[1]); if (isset($parts[2])) { $parts[2] = str_replace(' ', '+', $parts[2]); } $token = implode('.', $parts); } return $token; } protected function getUserFromToken(string $token): ?object { $payload = $this->decodeTokenPayload($token); if (!is_object($payload)) { return null; } $user = $payload->user ?? null; if (!is_object($user)) { return null; } if (!isset($user->permissions) && isset($payload->permissions)) { $user->permissions = $payload->permissions; } if (!isset($user->all_data_access) && isset($payload->all_data_access)) { $user->all_data_access = $payload->all_data_access; } return $user; } protected function decodeTokenPayload(string $token): ?object { $token = $this->normalizeRawToken($token); $parts = explode('.', $token); if (count($parts) < 2 || $parts[1] === '') { return null; } $payloadSegment = strtr($parts[1], '-_', '+/'); $padding = strlen($payloadSegment) % 4; if ($padding > 0) { $payloadSegment .= str_repeat('=', 4 - $padding); } $payload = json_decode(base64_decode($payloadSegment)); return is_object($payload) ? $payload : null; } protected function userDomainIsAllowed(string $domain): bool { return in_array(strtolower($domain), self::ALLOWED_DOMAINS, true); } protected function isAllowedDomainUser(object $user): bool { $domain = strtolower((string) ($user->domain ?? '')); if ($this->userDomainIsAllowed($domain)) { return true; } $partition = strtolower((string) ($user->partition ?? '')); foreach (self::ALLOWED_DOMAINS as $allowedDomain) { if ($partition === 'rapport-' . $allowedDomain) { return true; } } return false; } protected function buildUrl(string $path, Request $request): string { $query = array_filter([ 'token' => $request->input('token') ?: $request->session()->get(self::SESSION_TOKEN_KEY), ]); $url = $this->_base_url . $path; if (!empty($query)) { $url .= '?' . http_build_query($query); } return $url; } protected function resolvePerPage(Request $request): int { $allowed = [25, 50, 100, 200]; $perPage = (int) $request->input('per_page', 50); return in_array($perPage, $allowed, true) ? $perPage : 50; } protected function resolveFilters(Request $request): array { $dateMode = $request->input('date_mode', 'single'); if (!in_array($dateMode, ['single', 'range'], true)) { $dateMode = 'single'; } return [ 'date_mode' => $dateMode, 'activity_date' => trim((string) $request->input('activity_date', '')), 'date_from' => trim((string) $request->input('date_from', '')), 'date_to' => trim((string) $request->input('date_to', '')), 'zone' => trim((string) $request->input('zone', '')), 'state' => trim((string) $request->input('state', '')), 'educator_hq' => trim((string) $request->input('educator_hq', '')), 'educator_name' => trim((string) $request->input('educator_name', '')), 'per_page' => (string) $this->resolvePerPage($request), ]; } protected function buildFilteredQuery(array $filters) { $query = DB::table(self::ACTIVITIES_TABLE); if ($filters['date_mode'] === 'range') { $from = $this->normalizeFilterDate($filters['date_from']); $to = $this->normalizeFilterDate($filters['date_to']); if ($from) { $query->where('activity_date', '>=', $from); } if ($to) { $query->where('activity_date', '<=', $to); } } else { $date = $this->normalizeFilterDate($filters['activity_date']); if ($date) { $query->where('activity_date', $date); } } foreach (['zone', 'state', 'educator_hq', 'educator_name'] as $field) { if ($filters[$field] !== '') { $query->where($field, $filters[$field]); } } return $query; } protected function filterOptions(): array { $zones = DB::table(self::ACTIVITIES_TABLE)->whereNotNull('zone')->where('zone', '!=', '')->distinct()->orderBy('zone')->pluck('zone'); $states = DB::table(self::ACTIVITIES_TABLE)->whereNotNull('state')->where('state', '!=', '')->distinct()->orderBy('state')->pluck('state'); $hqs = DB::table(self::ACTIVITIES_TABLE)->whereNotNull('educator_hq')->where('educator_hq', '!=', '')->distinct()->orderBy('educator_hq')->pluck('educator_hq'); $educators = DB::table(self::ACTIVITIES_TABLE)->whereNotNull('educator_name')->where('educator_name', '!=', '')->distinct()->orderBy('educator_name')->pluck('educator_name'); return [ 'zones' => $zones, 'states' => $states, 'educator_hqs' => $hqs, 'educator_names' => $educators, ]; } protected function normalizeFilterDate(string $value): ?string { if ($value === '') { return null; } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { return $value; } return $this->parseActivityDate($value); } protected function parseActivityDate(?string $value): ?string { if ($value === null || trim($value) === '') { return null; } $value = trim($value); if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { return $value; } // Two-digit year formats must be tried before d/m/Y — otherwise "01/07/26" becomes year 0026. foreach (['d/m/y', 'j/n/y', 'd-m-y', 'd/m/Y', 'j/n/Y', 'd-m-Y'] as $format) { try { $parsed = Carbon::createFromFormat('!' . $format, $value); $year = (int) $parsed->format('Y'); if ($year >= 1970 && $year <= 2100) { return $parsed->format('Y-m-d'); } } catch (\Throwable $e) { continue; } } if (is_numeric($value)) { $numeric = (float) $value; if ($numeric > 30000 && $numeric < 60000) { try { return ExcelDate::excelToDateTimeObject($numeric)->format('Y-m-d'); } catch (\Throwable $e) { // fall through } } } if (preg_match('/^\d{4}-\d{2}-\d{2}/', $value)) { try { $parsed = Carbon::parse($value); $year = (int) $parsed->format('Y'); if ($year >= 1970 && $year <= 2100) { return $parsed->format('Y-m-d'); } } catch (\Throwable $e) { return null; } } return null; } protected function formatDisplayDate(?string $value): string { if ($value === null || $value === '') { return ''; } try { return Carbon::parse($value)->format('d/m/Y'); } catch (\Throwable $e) { return $value; } } protected function parseImportFile($file): array { $extension = strtolower($file->getClientOriginalExtension()); if (in_array($extension, ['xlsx', 'xls'], true)) { $data = Excel::toArray([], $file); if (empty($data[0])) { return ['headers' => [], 'data' => []]; } $sheet = $data[0]; $headers = array_shift($sheet); return ['headers' => $headers, 'data' => $sheet]; } $lines = array_map('str_getcsv', file($file->getRealPath())); $headers = array_shift($lines) ?: []; return ['headers' => $headers, 'data' => $lines]; } protected function normalizeHeader(string $header): string { $header = preg_replace('/^\xEF\xBB\xBF/', '', $header); $header = strtolower(trim(preg_replace('/\s+/', ' ', $header))); return $header; } protected function mapHeaderIndexes(array $headers): array { $map = []; foreach ($headers as $index => $header) { $field = $this->headerToField($this->normalizeHeader((string) $header)); if ($field) { $map[$field] = $index; } } return $map; } protected function headerToField(string $normalized): ?string { $aliases = [ 'date' => 'activity_date', 'state' => 'state', 'zone' => 'zone', 'educator hq' => 'educator_hq', 'educator name' => 'educator_name', 'educator manager' => 'educator_manager', 'camp area' => 'camp_area', 'time' => 'activity_time', 'msl code' => 'msl_code', 'doctor name' => 'doctor_name', 'doctor speciality' => 'doctor_speciality', 'doctor specialty' => 'doctor_speciality', 'chemist name' => 'chemist_name', ]; return $aliases[$normalized] ?? null; } protected function mapRowToRecord(array $row, array $columnIndex): array { $get = function (string $field) use ($row, $columnIndex) { if (!isset($columnIndex[$field])) { return null; } $value = $row[$columnIndex[$field]] ?? null; if ($value === null || $value === '') { return null; } return $this->sanitizeDbString((string) $value); }; $rawDate = isset($columnIndex['activity_date']) ? ($row[$columnIndex['activity_date']] ?? null) : null; $activityDate = $this->parseActivityDate($this->normalizeImportDateValue($rawDate)); return [ 'activity_date' => $activityDate, 'state' => $get('state'), 'zone' => $get('zone'), 'educator_hq' => $get('educator_hq'), 'educator_name' => $get('educator_name'), 'educator_manager' => $get('educator_manager'), 'camp_area' => $get('camp_area'), 'activity_time' => $get('activity_time'), 'msl_code' => $get('msl_code'), 'doctor_name' => $get('doctor_name'), 'doctor_speciality' => $get('doctor_speciality'), 'chemist_name' => $get('chemist_name'), ]; } protected function normalizeImportDateValue($value): ?string { if ($value === null || $value === '') { return null; } if ($value instanceof \DateTimeInterface) { return $value->format('Y-m-d'); } if (is_numeric($value)) { $numeric = (float) $value; if ($numeric > 30000 && $numeric < 60000) { try { return ExcelDate::excelToDateTimeObject($numeric)->format('Y-m-d'); } catch (\Throwable $e) { // fall through to string parsing } } } return $this->sanitizeDbString((string) $value); } protected function sanitizeDbString(?string $value): ?string { if ($value === null) { return null; } $value = str_replace("\0", '', $value); if (!mb_check_encoding($value, 'UTF-8')) { $converted = @iconv('Windows-1252', 'UTF-8//IGNORE', $value); if ($converted !== false && $converted !== '') { $value = $converted; } } $value = trim($value); return $value === '' ? null : $value; } protected function formatRowError(int $lineNo, \Throwable $e): string { $message = $this->sanitizeDbString($e->getMessage()) ?: 'Unknown row error.'; if (strlen($message) > 240) { $message = substr($message, 0, 240) . '...'; } return "Row {$lineNo}: {$message}"; } protected function updateImportLog(int $importId, array $payload): void { if (array_key_exists('error_log', $payload)) { $payload['error_log'] = $payload['error_log'] === null ? null : $this->sanitizeDbString((string) $payload['error_log']); } DB::table(self::IMPORTS_TABLE)->where('id', $importId)->update($payload); } }