/titansbrandunitsreport/report?token=TOKEN */ class TitansbrandunitsreportController extends Controller { private const DOMAIN = 'titans'; private const PARTITION = 'rapport-titans'; private const REPORT_START = 'April 2026'; private const BRAND_GROUPS = [ 'Foracort G' => [ 'Rotacaps' => '1783578705-ef9fb1c4-fd99-4fba-8cfc-6dd41a5121ad', 'Synchrobreathe' => '1783623548-9ad28a00-2311-40a4-a612-81cbb5e4a11f', 'Inhaler' => '1783623610-8fa0ce11-586f-4e73-ad74-b76a5b747014', 'Ciphaler' => '1783623678-d1c00c5a-9a81-4975-9157-452a7244a917', ], 'Glycohale F' => [ 'Rotacaps' => '1783623967-96f5e880-229d-4727-a9ef-976ad03a84dd', 'Synchrobreathe' => '1783623989-e8042064-88ad-444e-8e42-8180fe1021d7', 'Inhaler' => '1783623943-ad4a354f-b3f0-430a-93ac-21b7efcaa1fb', ], 'Glycohale FB' => [ 'Rotacaps' => '1783624207-51f0122a-61d7-43b2-9472-a543ef793f94', 'Synchrobreathe' => '1783624199-f359b6e4-ef93-4f22-a494-32250bb9e8dc', 'Inhaler' => '1783624218-3b9e4b8c-7ddf-41b1-b764-2f85673b435e', ], ]; private ?string $accessToken = null; private ?object $tokenUser = null; public function __construct() { $this->_service = 'reports'; $this->_module = 'contact_metric_monthly'; $this->_viewname = 'titansbrandunitsreport'; $this->_title = 'Titans Brand Units Report'; $this->_base_url = '/titansbrandunitsreport'; } public function list(Request $request) { $this->bootstrapTokenSession($request); if ($request->hasSession()) { $request->session()->save(); } return redirect($this->buildUrl('/report', $request)); } public function report(Request $request) { $this->bootstrapTokenSession($request); $token = $this->assertTitansAccess($request); if ($request->hasSession()) { $request->session()->save(); } $perPage = $this->resolvePerPage($request); $search = $this->resolveSearchTerm($request); $payload = $this->buildReportPayload($request, false, $perPage, $search); $response = LambdaRequest::getrequest( $this->_service, $this->_module . '/doctor_units', $payload, $token, 300 ); $reportError = $this->resolveReportError($response); if ($reportError !== null) { if ($this->isAuthReportError($reportError)) { $this->forgetPersistedToken($request); abort(401, $reportError); } abort(502, $reportError); } $report = $response->data ?? (object) []; [$doctors, $data, $pagination] = $this->resolveReportPage($report, $request, $perPage, $search); return $this->view('report')->with([ 'title' => $this->_title, 'base_url' => $this->_base_url, 'token' => $token, 'brandGroups' => $this->brandGroupsForView(), 'months' => $report->months ?? [], 'doctors' => $doctors, 'data' => $data, 'pagination' => $pagination, 'perPage' => $perPage, 'search' => $search, ]); } public function export(Request $request) { $this->bootstrapTokenSession($request); $token = $this->assertTitansAccess($request); $search = $this->resolveSearchTerm($request); $payload = $this->buildReportPayload($request, false, 200, $search); unset($payload['page'], $payload['per_page']); $response = LambdaRequest::getrequest( $this->_service, $this->_module . '/doctor_units_export', $payload, $token, 60 ); $reportError = $this->resolveReportError($response); if ($reportError !== null) { if ($this->isAuthReportError($reportError)) { $this->forgetPersistedToken($request); abort(401, $reportError); } abort(502, $reportError); } $export = $response->data ?? (object) []; $exportId = trim((string) ($export->export_id ?? '')); if ($exportId === '') { abort(502, 'Export failed: the report service did not return an export id.'); } return $this->view('export')->with([ 'title' => $this->_title . ' Export', 'base_url' => $this->_base_url, 'export_id' => $exportId, 'search' => $search, 'token' => $token, ]); } public function exportStatus(Request $request) { $token = $this->assertTitansAccess($request); $exportId = trim((string) $request->query('export_id', '')); if ($exportId === '') { return response()->json(['status' => 'failed', 'message' => 'export_id is required'], 400); } $response = LambdaRequest::getrequest( $this->_service, $this->_module . '/doctor_units_export', ['export_id' => $exportId], $token, 30 ); $reportError = $this->resolveReportError($response); if ($reportError !== null) { if ($this->isAuthReportError($reportError)) { $this->forgetPersistedToken($request); return response()->json(['status' => 'failed', 'message' => $reportError], 401); } return response()->json(['status' => 'failed', 'message' => $reportError], 502); } $status = $response->data ?? (object) []; return response()->json([ 'export_id' => $status->export_id ?? $exportId, 'status' => $status->status ?? 'processing', 'url' => $status->url ?? null, 'row_count' => (int) ($status->row_count ?? $status->rows ?? 0), 'rows' => (int) ($status->rows ?? 0), 'batches' => (int) ($status->batches ?? 0), 'message' => $status->message ?? null, ]); } protected function brandGroupsForView(): array { $groups = []; foreach (self::BRAND_GROUPS as $groupName => $subBrands) { $brands = []; foreach ($subBrands as $subBrandName => $metricId) { $brands[] = (object) [ 'name' => $subBrandName, 'metric_id' => $metricId, ]; } $groups[] = (object) [ 'name' => $groupName, 'brands' => $brands, ]; } return $groups; } protected function flatMetrics(): array { $metrics = []; foreach (self::BRAND_GROUPS as $groupName => $subBrands) { foreach ($subBrands as $subBrandName => $metricId) { $metrics[$metricId] = $groupName . ' - ' . $subBrandName; } } return $metrics; } protected function resolveReportPage(object $report, Request $request, int $perPage, string $search = ''): array { $doctors = $this->normalizeDoctorsList($report->doctors ?? null); if ($search !== '') { $doctors = $this->filterDoctorsBySearch($doctors, $search); } $data = $report->data ?? (object) []; $page = max(1, (int) $request->input('page', 1)); $apiPagination = $report->pagination ?? null; $apiPaginated = is_object($apiPagination) && isset($apiPagination->total_pages) && (int) $apiPagination->total_pages >= 0 && ($search !== '' || count($doctors) <= $perPage); if ($apiPaginated) { $apiPagination->page = (int) ($apiPagination->page ?? $page); $apiPagination->per_page = (int) ($apiPagination->per_page ?? $perPage); return [$doctors, $data, $apiPagination]; } $total = count($doctors); $totalPages = max(1, (int) ceil($total / $perPage)); $page = min($page, $totalPages); $offset = ($page - 1) * $perPage; $pagedDoctors = array_slice($doctors, $offset, $perPage); $pagedData = (object) []; foreach ($pagedDoctors as $doctor) { $doctorId = is_object($doctor) ? ($doctor->id ?? null) : null; if ($doctorId && isset($data->{$doctorId})) { $pagedData->{$doctorId} = $data->{$doctorId}; } } $pagination = (object) [ 'page' => $page, 'per_page' => $perPage, 'total' => $total, 'total_pages' => $totalPages, ]; return [$pagedDoctors, $pagedData, $pagination]; } protected function resolveSearchTerm(Request $request): string { return trim((string) $request->query('search', '')); } protected function filterDoctorsBySearch(array $doctors, string $search): array { if ($search === '') { return $doctors; } $needle = mb_strtolower($search); $words = preg_split('/\s+/u', $needle, -1, PREG_SPLIT_NO_EMPTY); if (empty($words)) { return $doctors; } return array_values(array_filter($doctors, function ($doctor) use ($words) { if (!is_object($doctor)) { return false; } $haystack = $this->doctorSearchHaystack($doctor); foreach ($words as $word) { if (!str_contains($haystack, $word)) { return false; } } return true; })); } protected function doctorSearchHaystack(object $doctor): string { $parts = [ $doctor->name ?? $doctor->id ?? '', $doctor->msl_code ?? '', $doctor->user ?? '', $doctor->user_firstname ?? '', $doctor->user_lastname ?? '', $doctor->contact_user_name ?? '', $doctor->employee_code ?? '', $doctor->zone ?? '', $doctor->region ?? '', $doctor->area ?? '', $doctor->territory ?? '', ]; $normalized = []; foreach ($parts as $part) { $value = trim((string) $part); if ($value === '' || $value === '-') { continue; } $normalized[] = mb_strtolower($value); } return preg_replace('/\s+/u', ' ', implode(' ', $normalized)); } protected function normalizeDoctorsList($raw): array { $doctors = []; if (is_array($raw)) { $doctors = array_values($raw); } elseif (is_object($raw)) { $doctors = array_values((array) $raw); } return array_values(array_filter($doctors, function ($doctor) { if (!is_object($doctor)) { return false; } $name = trim((string) ($doctor->name ?? '')); $id = trim((string) ($doctor->id ?? '')); return $name !== '' && $name !== $id; })); } protected function buildReportPayload(Request $request, bool $fetchAll = false, int $perPage = 50, string $search = ''): array { $metrics = $this->flatMetrics(); $metricLabels = []; foreach ($metrics as $id => $label) { $metricLabels[$id] = $label; } $payload = [ 'partition' => self::PARTITION, 'metric_ids' => json_encode(array_keys($metrics)), 'metric_labels' => json_encode($metricLabels), 'start_month' => self::REPORT_START, 'column_layout' => 'metric_month', 'include_tagging_columns' => '0', 'doctor_source' => 'metric', ]; if ($search !== '') { $payload['search'] = $search; } if ($fetchAll) { $payload['all'] = '1'; } else { $payload['page'] = max(1, (int) $request->input('page', 1)); $payload['per_page'] = $perPage; } return $payload; } protected function resolvePerPage(Request $request): int { $allowed = [25, 50, 100, 200]; $default = 50; $raw = $request->query('per_page'); if ($raw !== null && $raw !== '') { $perPage = (int) $raw; if (!in_array($perPage, $allowed, true)) { $perPage = $default; } $request->session()->put('titans_brand_units_report_per_page', $perPage); return $perPage; } if ($request->session()->has('titans_brand_units_report_per_page')) { $perPage = (int) $request->session()->get('titans_brand_units_report_per_page'); if (in_array($perPage, $allowed, true)) { return $perPage; } } return $default; } protected function assertTitansAccess(Request $request): string { $token = $this->resolveAccessToken($request); if (!$token) { 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.'); } $payload = $this->decodeTokenPayload($token); $user = is_object($payload) ? ($payload->user ?? null) : null; if (!$user || !$this->isTitansUser($user)) { abort(403, 'Access denied. A valid titans 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->tokenUser = $user; return $token; } protected function bootstrapTokenSession(Request $request): void { $token = $this->extractRequestToken($request); if ($token === '') { return; } $user = $this->getUserFromToken($token); if ($user && $this->isTitansUser($user)) { $payload = $this->decodeTokenPayload($token); if (!$this->isTokenPayloadExpired($payload)) { $this->persistAccessToken($request, $token); } } } protected function isTitansUser(object $user): bool { return strtolower($user->domain ?? '') === self::DOMAIN || strtolower($user->partition ?? '') === self::PARTITION; } protected function normalizeRawToken(string $token): string { $token = trim($token); if (stripos($token, 'Bearer ') === 0) { $token = trim(substr($token, 7)); } return $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->isTitansUser($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->isTitansUser($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('titans_brand_units_report_token', $token); $this->tokenStorage()->put($this->tokenStoragePath($request), $token); } protected function loadPersistedToken(Request $request): string { if ($request->session()->has('titans_brand_units_report_token')) { $sessionToken = $this->normalizeRawToken((string) $request->session()->get('titans_brand_units_report_token')); 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('titans_brand_units_report_token'); $path = $this->tokenStoragePath($request); if ($this->tokenStorage()->exists($path)) { $this->tokenStorage()->delete($path); } } protected function tokenStoragePath(Request $request): string { return 'titans_brand_units_report_tokens/' . hash('sha256', $request->session()->getId()); } protected function tokenStorage() { return Storage::disk('local'); } protected function getUserFromToken(string $token): ?object { $payload = $this->decodeTokenPayload($token); if (!is_object($payload)) { return null; } return $payload->user ?? null; } protected function decodeTokenPayload(string $token): ?object { $token = $this->normalizeRawToken($token); $parts = explode('.', $token); if (count($parts) < 2) { 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 isTokenPayloadExpired(?object $payload): bool { if (!is_object($payload) || !isset($payload->exp)) { return false; } return (int) $payload->exp < time(); } protected function resolveReportError($response): ?string { if (!is_object($response)) { return 'Report service returned an invalid response.'; } $status = strtolower((string) ($response->status ?? '')); $statusCode = (int) ($response->statusCode ?? 0); if ($status !== 'failed' && ($statusCode === 0 || $statusCode < 400)) { return null; } $message = trim((string) ($response->message ?? '')); if ($message === '') { $message = 'Failed to load report data.'; } if ($this->isAuthReportError($message) || $statusCode === 401 || $statusCode === 403) { return 'Your access token has expired or is invalid. Please open a fresh link from the app.'; } return $message; } protected function isAuthReportError(string $message): bool { $normalized = strtolower($message); return str_contains($normalized, 'unauthorized') || str_contains($normalized, 'expired') || str_contains($normalized, 'invalid token'); } protected function buildUrl(string $path, Request $request): string { $perPage = $this->resolvePerPage($request); $query = array_filter([ 'page' => $request->input('page'), 'per_page' => $perPage, 'search' => $this->resolveSearchTerm($request), ], function ($value) { return $value !== null && $value !== ''; }); $token = $this->extractRequestToken($request); if ($token === '') { $token = $this->loadPersistedToken($request); } if ($token !== '') { $query['token'] = $token; } $url = $this->_base_url . $path; if (!empty($query)) { $url .= '?' . http_build_query($query); } return $url; } }