490 lines
17 KiB
PHP
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;
|
|
}
|
|
}
|