464 lines
14 KiB
PHP
464 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Http\Requests\LambdaRequest;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class PbsdoctoranalyticsReportController extends Controller
|
|
{
|
|
private const PARTITION = 'rapport-pulmocare';
|
|
|
|
private const REPORT_START = 'April 2026';
|
|
|
|
private const METRIC_PBC = '1782472261-38d31258-0708-46f0-a65f-6e2511b18d8b';
|
|
|
|
private const METRIC_TOTAL = '1782472577-483a9a7f-7c53-479e-a0cb-00d400d55f30';
|
|
|
|
private const METRIC_PULMO_DPI_PMDI = '1782472485-f9f2099e-4b6e-43d8-9212-62022ac97f55';
|
|
|
|
private const METRIC_PULMO_NEB = '1782472500-115764ec-d4eb-4c0f-b729-3ad7270159a6';
|
|
|
|
private const METRIC_PULMO_G = '1782472518-1edbc500-ad25-40fd-b56f-dd21ce4f8be9';
|
|
|
|
private const METRIC_PULMO_GROUP = '1782472533-60e26841-e935-4a9f-b0f3-f8a2f72ec1d4';
|
|
|
|
private const METRIC_BUDASURE = '1782472550-ce2be779-d7c2-4558-b2d6-c7ab0f353ef8';
|
|
|
|
private const METRIC_VILORESP_U = '1782472564-9d256aff-3f8f-440e-88c5-66f26e1b5e06';
|
|
|
|
private const METRIC_PS_SHARE = '1782472596-4ab4486a-6f2f-48da-861b-a59b33167ad2';
|
|
|
|
private const DEFAULT_BRAND = 'pulmosamrt_group';
|
|
|
|
/** Brand filter options → actual-value metric IDs */
|
|
private const BRANDS = [
|
|
'pulmosamrt_group' => [
|
|
'id' => self::METRIC_PULMO_GROUP,
|
|
'label' => 'Pulmosamrt Group',
|
|
],
|
|
'pulmosmart_dpi_pmdi' => [
|
|
'id' => self::METRIC_PULMO_DPI_PMDI,
|
|
'label' => 'Pulmosmart DPI & pMDI',
|
|
],
|
|
'pulmosmart_neb' => [
|
|
'id' => self::METRIC_PULMO_NEB,
|
|
'label' => 'Pulmosmart Neb',
|
|
],
|
|
'pulmosmart_g' => [
|
|
'id' => self::METRIC_PULMO_G,
|
|
'label' => 'Pulmosmart G',
|
|
],
|
|
'budasure' => [
|
|
'id' => self::METRIC_BUDASURE,
|
|
'label' => 'Budasure',
|
|
],
|
|
'viloresp_u' => [
|
|
'id' => self::METRIC_VILORESP_U,
|
|
'label' => 'Viloresp U',
|
|
],
|
|
];
|
|
|
|
private const METRICS = [
|
|
self::METRIC_PBC => 'PBC Amt',
|
|
self::METRIC_TOTAL => 'Total (Actual Value)',
|
|
self::METRIC_PULMO_DPI_PMDI => 'Pulmosmart DPI & pMDI (Actual Value)',
|
|
self::METRIC_PULMO_NEB => 'Pulmosmart Neb (Actual Value)',
|
|
self::METRIC_PULMO_G => 'Pulmosmart G (Actual Value)',
|
|
self::METRIC_PULMO_GROUP => 'Pulmosamrt Group (Actual Value)',
|
|
self::METRIC_BUDASURE => 'Budasure (Actual Value)',
|
|
self::METRIC_VILORESP_U => 'Viloresp U (Actual Value)',
|
|
self::METRIC_PS_SHARE => '% PS Share',
|
|
];
|
|
|
|
private ?string $accessToken = null;
|
|
|
|
private ?object $tokenUser = null;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->_service = 'reports';
|
|
$this->_module = 'pbs_doctor_analytics';
|
|
$this->_viewname = 'pbsdoctoranalyticsreport';
|
|
$this->_title = 'PBC Doctor Analytics';
|
|
$this->_base_url = '/pbsdoctoranalyticsreport';
|
|
}
|
|
|
|
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->assertPulmocareAccess($request);
|
|
if ($request->hasSession()) {
|
|
$request->session()->save();
|
|
}
|
|
|
|
$payload = $this->buildReportPayload($request);
|
|
$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) [];
|
|
$allDataAccess = $this->hasAllDataAccess($this->tokenUser);
|
|
$hierarchies = $allDataAccess ? $this->loadHierarchyOptions($token) : [];
|
|
|
|
$brands = [];
|
|
foreach (self::BRANDS as $key => $brand) {
|
|
$brands[] = [
|
|
'key' => $key,
|
|
'id' => $brand['id'],
|
|
'label' => $brand['label'],
|
|
];
|
|
}
|
|
|
|
return $this->view('report')->with([
|
|
'title' => $this->_title,
|
|
'base_url' => $this->_base_url,
|
|
'token' => $token,
|
|
'allDataAccess' => $allDataAccess,
|
|
'hierarchies' => $hierarchies,
|
|
'months' => $report->months ?? [],
|
|
'metrics' => $report->metrics ?? [],
|
|
'doctors' => $report->doctors ?? [],
|
|
'data' => $report->data ?? (object) [],
|
|
'brands' => $brands,
|
|
'defaultBrand' => self::DEFAULT_BRAND,
|
|
'metricIds' => [
|
|
'pbc' => self::METRIC_PBC,
|
|
'total' => self::METRIC_TOTAL,
|
|
'pulmo' => self::METRIC_PULMO_GROUP,
|
|
'pct' => self::METRIC_PS_SHARE,
|
|
],
|
|
]);
|
|
}
|
|
|
|
protected function buildReportPayload(Request $request): array
|
|
{
|
|
$metricLabels = [];
|
|
foreach (self::METRICS as $id => $label) {
|
|
$metricLabels[$id] = $label;
|
|
}
|
|
|
|
$startMonth = trim((string) $request->query('start_month', self::REPORT_START));
|
|
if ($startMonth === '') {
|
|
$startMonth = self::REPORT_START;
|
|
}
|
|
|
|
$payload = [
|
|
'partition' => self::PARTITION,
|
|
'metric_ids' => json_encode(array_keys(self::METRICS)),
|
|
'metric_labels' => json_encode($metricLabels),
|
|
'start_month' => $startMonth,
|
|
'all' => '1',
|
|
];
|
|
|
|
$hierarchyIds = $request->query('hierarchy_ids');
|
|
if ($hierarchyIds !== null && $hierarchyIds !== '') {
|
|
$payload['hierarchy_ids'] = is_array($hierarchyIds)
|
|
? json_encode(array_values($hierarchyIds))
|
|
: (string) $hierarchyIds;
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
protected function hasAllDataAccess(?object $user): bool
|
|
{
|
|
if (!$user) {
|
|
return false;
|
|
}
|
|
|
|
$flag = $user->all_data_access ?? null;
|
|
|
|
return $flag === true
|
|
|| $flag === 1
|
|
|| $flag === '1'
|
|
|| (is_string($flag) && strtolower($flag) === 'true');
|
|
}
|
|
|
|
protected function loadHierarchyOptions(string $token): array
|
|
{
|
|
$response = LambdaRequest::getrequest(
|
|
'ums',
|
|
'hierarchy/hierarchy_below_me',
|
|
[
|
|
'partition' => self::PARTITION,
|
|
'hierarchy_id' => 'root',
|
|
],
|
|
$token
|
|
);
|
|
|
|
if (!is_object($response) || !isset($response->data->hierarchies)) {
|
|
return [];
|
|
}
|
|
|
|
$hierarchies = $response->data->hierarchies;
|
|
if (!is_array($hierarchies)) {
|
|
return [];
|
|
}
|
|
|
|
return array_values(array_filter($hierarchies, function ($hierarchy) {
|
|
return is_object($hierarchy) && !empty($hierarchy->id);
|
|
}));
|
|
}
|
|
|
|
protected function assertPulmocareAccess(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->isPulmocareUser($user)) {
|
|
abort(403, 'Access denied. A valid pulmocare 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->isPulmocareUser($user)) {
|
|
$payload = $this->decodeTokenPayload($token);
|
|
if (!$this->isTokenPayloadExpired($payload)) {
|
|
$this->persistAccessToken($request, $token);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function isPulmocareUser(object $user): bool
|
|
{
|
|
return ($user->domain ?? '') === 'pulmocare'
|
|
|| str_starts_with((string) ($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->isPulmocareUser($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->isPulmocareUser($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('pbs_doctor_analytics_report_token', $token);
|
|
$this->tokenStorage()->put($this->tokenStoragePath($request), $token);
|
|
}
|
|
|
|
protected function loadPersistedToken(Request $request): string
|
|
{
|
|
if ($request->session()->has('pbs_doctor_analytics_report_token')) {
|
|
$sessionToken = $this->normalizeRawToken((string) $request->session()->get('pbs_doctor_analytics_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('pbs_doctor_analytics_report_token');
|
|
$path = $this->tokenStoragePath($request);
|
|
if ($this->tokenStorage()->exists($path)) {
|
|
$this->tokenStorage()->delete($path);
|
|
}
|
|
}
|
|
|
|
protected function tokenStoragePath(Request $request): string
|
|
{
|
|
return 'pbs_doctor_analytics_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
|
|
{
|
|
$query = array_filter([
|
|
'start_month' => $request->query('start_month'),
|
|
], 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;
|
|
}
|
|
}
|