Rapport-Laravel/app/Http/Controllers/RevesmartlaunchdriveControl...

490 lines
17 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Exports\RevesmartLaunchDriveExport;
use App\Http\Requests\LambdaRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
/**
* Revesmart Launch Drive report (team3).
*
* Quick Action (Rapport admin, team3 partition):
* Type: external_with_token
* URL: https://<laravel-domain>/revesmartlaunchdrive/report?token=TOKEN
* Optional: &form_id=<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<int, array<int, mixed>>, 1: array<int, array{type:string}>, 2: array<int, string>}
*/
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;
}
}