/revesmartlaunchdrive/report?token=TOKEN * Optional: &form_id= * Month defaults to current month; page selector lists June 2026 → current. */ class RevesmartlaunchdriveController extends Controller { private const PARTITION = 'rapport-team3'; private const SESSION_TOKEN_KEY = 'revesmart_launch_drive_token'; /** Default Revesmart form (neb2); override via ?form_id= */ private const DEFAULT_FORM_ID = '1782972523-9113f86f-0d2e-4b78-81e8-ca5f14f6a0e1'; private ?string $accessToken = null; private ?object $tokenUser = null; public function __construct() { $this->_service = 'reports'; $this->_module = 'team3_reports'; $this->_viewname = 'revesmartlaunchdrive'; $this->_title = 'Revesmart-Launch-Drive'; $this->_base_url = '/revesmartlaunchdrive'; } public function list(Request $request) { return redirect($this->buildUrl('/report', $request)); } public function report(Request $request) { $token = $this->assertTeam3Access($request); $monthOptions = $this->buildMonthOptions(); $month = $this->resolveMonth($request, $monthOptions); $formId = $this->resolveFormId($request); $payloadData = $this->fetchReportData($token, $month, $formId, $request); return $this->view('report')->with([ 'title' => $this->_title, 'base_url' => $this->_base_url, 'token' => $token, 'month' => $month, 'monthOptions' => $monthOptions, 'formId' => $formId, 'currentMonth' => $payloadData['currentMonth'], 'yesterdayLabel' => $this->formatYesterdayLabel(), 'mtdLabel' => $this->formatMtdLabel($month), 'chestLabel' => $this->formatChestLabel($month), 'zbmReport' => $payloadData['zbmReport'], 'grandTotal' => $payloadData['grandTotal'], ]); } public function export(Request $request) { $token = $this->assertTeam3Access($request); $monthOptions = $this->buildMonthOptions(); $month = $this->resolveMonth($request, $monthOptions); $formId = $this->resolveFormId($request); $payloadData = $this->fetchReportData($token, $month, $formId, $request); $yesterdayLabel = $this->formatYesterdayLabel(); $mtdLabel = $this->formatMtdLabel($month); $chestLabel = $this->formatChestLabel($month); [$rows, $rowMeta, $merges] = $this->buildExportRows( $payloadData['zbmReport'], $payloadData['grandTotal'], $payloadData['currentMonth'], $yesterdayLabel, $mtdLabel, $chestLabel ); $safeMonth = preg_replace('/\s+/', '-', trim($month)); $filename = 'Revesmart-Launch-Drive-' . $safeMonth . '.xlsx'; return Excel::download( new RevesmartLaunchDriveExport($rows, $rowMeta, $payloadData['currentMonth'], $filename, $merges), $filename ); } protected function resolveFormId(Request $request): string { $formId = trim((string) $request->input('form_id', '')); return $formId !== '' ? $formId : self::DEFAULT_FORM_ID; } /** * @return array{currentMonth:bool,zbmReport:array,grandTotal:mixed} */ protected function fetchReportData(string $token, string $month, string $formId, ?Request $request = null): array { $endpoint = $this->_module . '/revesmartform_report'; $params = [ 'month' => $month, 'form_id' => $formId, ]; $tokenParts = explode('.', $token); $debugPayload = [ 'service' => $this->_service, 'endpoint' => $endpoint, 'full_url' => 'https://rapportapi.click/' . $this->_service . '/' . $endpoint, 'query_params' => $params, 'headers' => [ 'Authorization' => $token, ], 'token_meta' => [ 'length' => strlen($token), 'parts_count' => count($tokenParts), 'has_three_parts' => count($tokenParts) === 3, 'preview_start' => substr($token, 0, 50), 'preview_end' => substr($token, -30), ], ]; Log::info('revesmartform_report outbound request', $debugPayload); // Add &debug_api=1 to the report URL to dump the exact API call and stop. if ($request && $request->boolean('debug_api')) { dd($debugPayload); } $response = LambdaRequest::getrequest( $this->_service, $endpoint, $params, $token, 300 ); if (!is_object($response) || ($response->status ?? '') !== 'success') { $message = is_object($response) ? (string) ($response->message ?? 'Failed to load Revesmart Launch Drive report') : 'Failed to load Revesmart Launch Drive report'; abort(502, $message); } $data = $response->data ?? (object) []; $report = $data->report ?? (object) []; return [ 'currentMonth' => filter_var($data->current_month ?? false, FILTER_VALIDATE_BOOLEAN), 'zbmReport' => is_array($report->zbmReport ?? null) ? $report->zbmReport : [], 'grandTotal' => $report->grandTotal ?? null, ]; } /** * @return array{0: array>, 1: array, 2: array} */ protected function buildExportRows(array $zbmReport, $grandTotal, bool $showYesterday, string $yesterdayLabel, string $mtdLabel, string $chestLabel = 'MTD Chest'): array { $statusCols = 4; $dayCols = 8; $mtdCols = 8; $chestCols = 7; $totalCols = $statusCols + ($showYesterday ? $dayCols : 0) + $mtdCols + $chestCols; $blank = static function (int $n) { return array_fill(0, $n, ''); }; $groupRow = array_merge( ['Revesmart Launch Status'], $blank($statusCols - 1), $showYesterday ? array_merge([$yesterdayLabel], $blank($dayCols - 1)) : [], array_merge([$mtdLabel], $blank($mtdCols - 1)), array_merge([$chestLabel], $blank($chestCols - 1)) ); $subRow = [ 'RBM/ZBM', 'MNP', 'Participation', '% Participation', ]; if ($showYesterday) { $subRow = array_merge($subRow, [ 'Count of Doctor', 'Launch to HCPs', 'Handwriting Activity', 'Dr Converted', 'No of Dr Rx', 'No of Foracort Rx', 'No of Chemist availability', 'POB I Units', ]); } $subRow = array_merge($subRow, [ 'Count of Doctor', 'Launch to HCPs', 'Handwriting Activity', 'Dr Converted', 'No of Dr Rx', 'No of Foracort Rx', 'No of Chemist availability', 'POB I Units', 'Count of Doctor', 'Launch to HCPs', 'Handwriting Activity', 'Dr Converted', 'No of Dr Rx', 'No of Foracort Rx', '% Chest Launch', ]); $rows = [$groupRow, $subRow]; $rowMeta = [ ['type' => 'group'], ['type' => 'sub'], ]; $merges = []; $col = 1; $merges[] = $this->excelRange($col, 1, $statusCols); $col += $statusCols; if ($showYesterday) { $merges[] = $this->excelRange($col, 1, $dayCols); $col += $dayCols; } $merges[] = $this->excelRange($col, 1, $mtdCols); $col += $mtdCols; $merges[] = $this->excelRange($col, 1, $chestCols); $metricRow = function ($name, $obj) use ($showYesterday) { $obj = is_object($obj) ? $obj : (object) ($obj ?: []); $yesterday = $obj->yesterday ?? (object) []; $mtd = $obj->mtd ?? (object) []; $mtdChest = $obj->mtdChest ?? (object) []; $row = [ $name, $this->exportNum($obj->mnp ?? 0), $this->exportNum($obj->participation ?? 0), $this->exportPct($obj->participation_percentage ?? null), ]; if ($showYesterday) { $row = array_merge($row, [ $this->exportNum($yesterday->countOfDoctor ?? 0), $this->exportNum($yesterday->launchToHcps ?? 0), $this->exportNum($yesterday->revesmartHandwritingActivity ?? 0), $this->exportNum($yesterday->doctorConverted ?? 0), $this->exportNum($yesterday->doctorRx ?? 0), $this->exportNum($yesterday->noOfRxForacort ?? 0), $this->exportNum($yesterday->chemistAvailability ?? 0), $this->exportNum($yesterday->pobUnits ?? 0), ]); } $row = array_merge($row, [ $this->exportNum($mtd->countOfDoctor ?? 0), $this->exportNum($mtd->launchToHcps ?? 0), $this->exportNum($mtd->revesmartHandwritingActivity ?? 0), $this->exportNum($mtd->doctorConverted ?? 0), $this->exportNum($mtd->doctorRx ?? 0), $this->exportNum($mtd->noOfRxForacort ?? 0), $this->exportNum($mtd->chemistAvailability ?? 0), $this->exportNum($mtd->pobUnits ?? 0), $this->exportNum($mtdChest->countOfDoctor ?? 0), $this->exportNum($mtdChest->launchToHcps ?? 0), $this->exportNum($mtdChest->revesmartHandwritingActivity ?? 0), $this->exportNum($mtdChest->doctorConverted ?? 0), $this->exportNum($mtdChest->doctorRx ?? 0), $this->exportNum($mtdChest->noOfRxForacort ?? 0), $this->exportPct($mtdChest->chestLaunchPercentage ?? null), ]); return $row; }; foreach ($zbmReport as $zbm) { $zbmObj = is_object($zbm) ? $zbm : (object) $zbm; $rows[] = $metricRow($zbmObj->zbm ?? '-', $zbmObj); $rowMeta[] = ['type' => 'zbm']; $rbms = is_array($zbmObj->rbms ?? null) ? $zbmObj->rbms : []; foreach ($rbms as $rbm) { $rbmObj = is_object($rbm) ? $rbm : (object) $rbm; $rbmName = ' ' . ($rbmObj->rbm ?? '-'); $rows[] = $metricRow($rbmName, $rbmObj); $rowMeta[] = ['type' => 'rbm']; } } if (!empty($grandTotal)) { $gt = is_object($grandTotal) ? $grandTotal : (object) $grandTotal; $rows[] = $metricRow($gt->zbm ?? 'Grand Total', $gt); $rowMeta[] = ['type' => 'grand']; } // Ensure every row has the same column count foreach ($rows as $i => $row) { if (count($row) < $totalCols) { $rows[$i] = array_pad($row, $totalCols, ''); } } return [$rows, $rowMeta, $merges]; } protected function excelRange(int $startCol, int $row, int $span): string { $start = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($startCol) . $row; $end = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($startCol + $span - 1) . $row; return $start . ':' . $end; } protected function exportNum($value) { if ($value === null || $value === '') { return 0; } return is_numeric($value) ? (0 + $value) : $value; } protected function exportPct($value): string { if ($value === null || $value === '') { return '-'; } return rtrim(rtrim(number_format((float) $value, 2, '.', ''), '0'), '.') . '%'; } /** * Months from June 2026 through the current calendar month (newest first). * * @return string[] */ protected function buildMonthOptions(): array { $start = strtotime('2026-06-01'); $end = strtotime(date('Y-m-01')); if ($end < $start) { $end = $start; } $options = ['Overall']; $cursor = $end; while ($cursor >= $start) { $options[] = date('F Y', $cursor); $cursor = strtotime('-1 month', $cursor); } return $options; } protected function resolveMonth(Request $request, array $monthOptions): string { $month = trim((string) $request->input('month', '')); if ($month !== '' && in_array($month, $monthOptions, true)) { return $month; } if (in_array('Overall', $monthOptions, true)) { return 'Overall'; } $current = date('F Y'); if (in_array($current, $monthOptions, true)) { return $current; } return $monthOptions[0] ?? $current; } protected function formatYesterdayLabel(): string { // Match API: on Monday show Saturday (skip Sunday), otherwise calendar yesterday. $daysToSubtract = ((int) date('w') === 1) ? 2 : 1; $ts = strtotime('-' . $daysToSubtract . ' day'); $day = (int) date('j', $ts); $suffix = 'th'; if (!in_array($day % 100, [11, 12, 13], true)) { $mod = $day % 10; if ($mod === 1) { $suffix = 'st'; } elseif ($mod === 2) { $suffix = 'nd'; } elseif ($mod === 3) { $suffix = 'rd'; } } return $day . $suffix . ' ' . date('F Y', $ts); } protected function formatMtdLabel(string $month): string { if (strcasecmp(trim($month), 'Overall') === 0) { return 'Overall'; } $parts = preg_split('/\s+/', trim($month)); $monthName = $parts[0] ?? date('F'); return 'MTD ' . $monthName; } protected function formatChestLabel(string $month): string { if (strcasecmp(trim($month), 'Overall') === 0) { return 'Overall Chest'; } return 'MTD Chest'; } protected function buildUrl(string $path, Request $request): string { $params = array_filter([ 'token' => $request->input('token') ?: $this->accessToken, 'month' => $request->input('month'), 'form_id' => $request->input('form_id'), ], static fn ($v) => $v !== null && $v !== ''); $query = http_build_query($params); return $this->_base_url . $path . ($query ? '?' . $query : ''); } protected function assertTeam3Access(Request $request): string { $token = $this->resolveAccessToken($request); if (!$token) { abort(401, 'Access token is required. Open this report with ?token=... from the app.'); } $this->accessToken = $token; return $token; } protected function isTeam3User(object $user): bool { return ($user->domain ?? '') === 'team3' || ($user->partition ?? '') === self::PARTITION; } protected function resolveAccessToken(Request $request): ?string { // URL / Authorization header only (no admin session fallback). $token = $this->normalizeRawToken( (string) ($request->input('token') ?? $request->header('Authorization') ?? '') ); if ($token === '' && $request->hasSession() && $request->session()->has(self::SESSION_TOKEN_KEY)) { $token = $this->normalizeRawToken((string) $request->session()->get(self::SESSION_TOKEN_KEY)); } if ($token !== '' && $request->hasSession()) { $request->session()->put(self::SESSION_TOKEN_KEY, $token); } return $token !== '' ? $token : null; } 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 { $parts = explode('.', $token); if (count($parts) < 2) { return null; } $payloadJson = base64_decode(strtr($parts[1], '-_', '+/'), true); if ($payloadJson === false) { return null; } $payload = json_decode($payloadJson); if (!is_object($payload)) { return null; } return $payload->user ?? null; } }