/titanshcpgatheringreport/report?token=TOKEN */ class TitanshcpgatheringreportController extends Controller { private const DOMAIN = 'titans'; private const SESSION_TOKEN_KEY = 'titans_hcp_gathering_report_token'; private ?string $accessToken = null; private ?object $reportUser = null; public function __construct() { $this->_viewname = 'titanshcpgatheringreport'; $this->_title = 'Titans HCP Gathering Report'; $this->_base_url = '/titanshcpgatheringreport'; $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() === 'titanshcpgatheringreport-list' && $request->filled('token')) { return $next($request); } if (!Auth::check()) { return redirect('/login'); } return $next($request); }); } public function list(Request $request) { if ($request->filled('token')) { return redirect($this->buildUrl('/report', $request)); } $this->assertTitansAdmin(); $imports = DB::table('titans_hcp_gathering_imports') ->orderByDesc('created_at') ->limit(100) ->get(); return $this->view('list')->with([ 'title' => $this->_title, 'base_url' => $this->_base_url, 'imports' => $imports, 'buttons' => $this->getListButtons(), ]); } public function import(Request $request) { $this->assertTitansAdmin(); return $this->view('import')->with([ 'title' => 'Import Titans HCP Data', 'base_url' => $this->_base_url, ]); } public function postimport(Request $request) { $this->assertTitansAdmin(); try { $validator = Validator::make($request->all(), [ 'report_month' => 'required|date_format:Y-m', 'import_file' => 'required|file|max:10240', ], [ 'report_month.required' => 'Please select a report month.', 'report_month.date_format' => 'Report month must be in YYYY-MM format.', '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.'); } $reportMonth = $request->input('report_month'); $importId = DB::table('titans_hcp_gathering_imports')->insertGetId([ 'file_name' => $file->getClientOriginalName(), 'report_month' => $reportMonth, '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['pcode']) || !isset($columnIndex['legacy_code'])) { throw new \RuntimeException('Required columns PCODE and LEGACY_CODE were 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['pcode']) || empty($record['legacy_code'])) { $skipped++; continue; } $existing = DB::table('titans_hcp_gathering') ->where('pcode', $record['pcode']) ->where('legacy_code', $record['legacy_code']) ->where('report_month', $reportMonth) ->first(); $payload = array_merge($record, [ 'report_month' => $reportMonth, 'import_id' => $importId, 'updated_at' => now(), ]); if ($existing) { DB::table('titans_hcp_gathering')->where('id', $existing->id)->update($payload); $replaced++; } else { DB::table('titans_hcp_gathering')->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).'; } if ($inserted === 0 && $replaced === 0) { $message .= ' No rows were imported. Check that PCODE and LEGACY_CODE columns are present and populated.'; } return redirect('/titanshcpgatheringreports')->with('message', $message); } catch (\Throwable $e) { Log::error('Titans HCP gathering 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.'); } } public function report(Request $request) { $token = $this->assertTitansAccess($request); $month = $this->resolveReportMonth($request); $months = $this->availableMonths(); $rows = $this->fetchReportRows($month); $tmVisitLabel = $this->tmVisitColumnLabel($month); return $this->view('report')->with([ 'title' => $this->_title, 'base_url' => $this->_base_url, 'token' => $token, 'month' => $month, 'months' => $months, 'rows' => $rows, 'tm_visit_label' => $tmVisitLabel, ]); } public function export(Request $request) { $this->assertTitansAccess($request); $month = $this->resolveReportMonth($request); $rows = $this->fetchReportRows($month); $tmVisitLabel = $this->tmVisitColumnLabel($month); $header = [ 'ZBM', 'RBM', 'AREA', 'HQ', 'TM Name', 'HCP NAME', 'LEGACY_CODE', 'PCODE', 'AgreementNo', 'Status', 'EAS ID', 'Activity Report Status', 'Payment Status', 'Closure Done', 'Closure Approved', $tmVisitLabel, ]; $excelData = [$header]; foreach ($rows as $row) { $excelData[] = [ $row->zbm, $row->rbm, $row->area, $row->hq, $row->tm_name, $row->hcp_name, $row->legacy_code, $row->pcode, $row->agreement_no, $row->status, $row->eas_id, $row->activity_report_status, $row->payment_status, $row->closure_done, $row->closure_approved, $row->tm_visit, ]; } $filename = 'titans-hcp-gathering-' . $month . '.xlsx'; return Excel::download(new InvoicesExport($excelData), $filename); } protected function getListButtons(): array { return [ ['title' => 'Import', 'link' => $this->_base_url . '/import'], ]; } protected function assertTitansAdmin(): void { $user = Auth::user(); $permissions = json_decode($user->permissions ?? '[]'); if (!is_array($permissions)) { $permissions = []; } if (($user->domain ?? '') !== self::DOMAIN) { abort(403); } if (!in_array('all_data_access', $permissions, true) && !in_array('formexports_get_all', $permissions, true)) { abort(403); } } protected function assertTitansAccess(Request $request): string { $token = $this->resolveAccessToken($request); if (!$token) { abort(401, 'Access token is required'); } $user = $this->getUserFromToken($token); if (!$user || !$this->isTitansUser($user)) { abort(403, 'Invalid access token or user is not in the Titans domain.'); } $this->accessToken = $token; $this->reportUser = $user; return $token; } protected function resolveAccessToken(Request $request): ?string { $fromRequest = $this->extractRequestToken($request); if ($fromRequest !== '') { $user = $this->getUserFromToken($fromRequest); if ($user && $this->isTitansUser($user)) { $this->persistAccessToken($request, $fromRequest); return $fromRequest; } } $storedToken = $this->loadPersistedToken($request); if ($storedToken !== '') { $user = $this->getUserFromToken($storedToken); if ($user && $this->isTitansUser($user)) { 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); } protected function loadPersistedToken(Request $request): string { if (!$request->session()->has(self::SESSION_TOKEN_KEY)) { return ''; } return $this->normalizeRawToken((string) $request->session()->get(self::SESSION_TOKEN_KEY)); } protected function forgetPersistedToken(Request $request): void { $request->session()->forget(self::SESSION_TOKEN_KEY); } protected function normalizeRawToken(string $token): string { $token = trim($token); if (stripos($token, 'Bearer ') === 0) { $token = trim(substr($token, 7)); } 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 isTitansUser(object $user): bool { $domain = strtolower((string) ($user->domain ?? '')); if ($domain === self::DOMAIN) { return true; } $partition = strtolower((string) ($user->partition ?? '')); return $partition === 'rapport-' . self::DOMAIN; } protected function buildUrl(string $path, Request $request): string { $query = array_filter([ 'token' => $request->input('token') ?: $request->session()->get(self::SESSION_TOKEN_KEY), 'month' => $request->input('month'), ]); $url = $this->_base_url . $path; if (!empty($query)) { $url .= '?' . http_build_query($query); } return $url; } protected function resolveReportMonth(Request $request): string { $month = $request->input('month'); if ($month && preg_match('/^\d{4}-\d{2}$/', $month)) { return $month; } $latest = DB::table('titans_hcp_gathering')->max('report_month'); return $latest ?: Carbon::now()->format('Y-m'); } protected function availableMonths(): array { $query = DB::table('titans_hcp_gathering') ->select('report_month') ->distinct(); $query = $this->applyReportUserScope($query, $this->reportUser); return $query ->orderByDesc('report_month') ->pluck('report_month') ->all(); } protected function fetchReportRows(string $month) { $query = DB::table('titans_hcp_gathering') ->where('report_month', $month); $query = $this->applyReportUserScope($query, $this->reportUser); return $query ->orderBy('hcp_name') ->orderBy('pcode') ->get(); } protected function userHasAllDataAccess(?object $user): bool { if (!$user) { return false; } if (!empty($user->all_data_access)) { return true; } $permissions = $user->permissions ?? '[]'; if (is_string($permissions)) { $permissions = json_decode($permissions, true); } if (!is_array($permissions)) { $permissions = []; } return in_array('all_data_access', $permissions, true); } protected function resolveScopedHierarchyIds(?object $user): array { if (!$user || empty($user->id)) { return []; } $partition = 'rapport-' . ($user->domain ?? self::DOMAIN); $hierarchyIds = []; if (!empty($user->hierarchy)) { $hierarchyIds[] = $user->hierarchy; } try { $response = LambdaRequest::getrequest('ums', 'hierarchy/hierarchy_below_me', [ 'partition' => $partition, 'user_id' => $user->id, ]); if (isset($response->data->hierarchies) && is_array($response->data->hierarchies)) { foreach ($response->data->hierarchies as $hierarchy) { if (!empty($hierarchy->id)) { $hierarchyIds[] = $hierarchy->id; } } } } catch (\Throwable $e) { Log::warning('Titans HCP report hierarchy scope failed', [ 'user_id' => $user->id ?? null, 'error' => $e->getMessage(), ]); } return array_values(array_unique(array_filter($hierarchyIds))); } protected function applyReportUserScope($query, ?object $user) { if (!$user || $this->userHasAllDataAccess($user)) { return $query; } $hierarchyIds = $this->resolveScopedHierarchyIds($user); $userId = (string) ($user->id ?? ''); return $query->where(function ($scopedQuery) use ($hierarchyIds, $userId) { if (empty($hierarchyIds) && $userId === '') { $scopedQuery->whereRaw('1 = 0'); return; } if (!empty($hierarchyIds)) { $scopedQuery->whereIn('hierarchy_id', $hierarchyIds); } if ($userId !== '') { $scopedQuery->orWhere('user_id', $userId); } }); } protected function tmVisitColumnLabel(string $month): string { try { return Carbon::createFromFormat('Y-m', $month)->format('F') . ' TM Visit'; } catch (\Throwable $e) { return 'TM Visit'; } } 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 = []; $tmVisitIndex = null; foreach ($headers as $index => $header) { $normalized = $this->normalizeHeader((string) $header); if (str_contains($normalized, 'tm visit')) { $tmVisitIndex = $index; continue; } $field = $this->headerToField($normalized); if ($field) { $map[$field] = $index; } } if ($tmVisitIndex !== null) { $map['tm_visit'] = $tmVisitIndex; } return $map; } protected function headerToField(string $normalized): ?string { $aliases = [ 's.no' => 'sno', 'sno' => 'sno', 's no' => 'sno', 'zbm' => 'zbm', 'rbm' => 'rbm', 'area' => 'area', 'hq' => 'hq', 'tm name' => 'tm_name', 'hcp name' => 'hcp_name', 'doctor_id' => 'doctor_id', 'doctor id' => 'doctor_id', 'hierarchy_id' => 'hierarchy_id', 'hierarchy id' => 'hierarchy_id', 'user_id' => 'user_id', 'user id' => 'user_id', 'legacy_code' => 'legacy_code', 'legacy code' => 'legacy_code', 'pcode' => 'pcode', 'agreementno' => 'agreement_no', 'agreement no' => 'agreement_no', 'status' => 'status', 'eas id' => 'eas_id', 'activity report status' => 'activity_report_status', 'payment status' => 'payment_status', 'closure done' => 'closure_done', 'closure approved' => 'closure_approved', ]; 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); }; $sno = $get('sno'); return [ 'sno' => is_numeric($sno) ? (int) $sno : null, 'zbm' => $get('zbm'), 'rbm' => $get('rbm'), 'area' => $get('area'), 'hq' => $get('hq'), 'tm_name' => $get('tm_name'), 'hcp_name' => $get('hcp_name'), 'doctor_id' => $get('doctor_id'), 'hierarchy_id' => $get('hierarchy_id'), 'user_id' => $get('user_id'), 'legacy_code' => $get('legacy_code'), 'pcode' => $get('pcode'), 'agreement_no' => $get('agreement_no'), 'status' => $get('status'), 'eas_id' => $get('eas_id'), 'activity_report_status' => $get('activity_report_status'), 'payment_status' => $get('payment_status'), 'closure_done' => $get('closure_done'), 'closure_approved' => $get('closure_approved'), 'tm_visit' => $get('tm_visit'), ]; } 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; } } if (function_exists('iconv')) { $converted = @iconv('UTF-8', 'UTF-8//IGNORE', $value); if ($converted !== false) { $value = $converted; } } elseif (function_exists('mb_convert_encoding')) { $value = mb_convert_encoding($value, 'UTF-8', 'UTF-8'); } $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value) ?? ''; $value = trim($value); return $value === '' ? null : $value; } protected function formatRowError(int $lineNo, \Throwable $e): string { $message = $e->getMessage(); if (str_contains($message, 'Incorrect string value')) { return "Row {$lineNo}: Invalid characters in row data."; } $message = $this->sanitizeDbString($message) ?: '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('titans_hcp_gathering_imports')->where('id', $importId)->update($payload); } }