Rapport-Laravel/app/Http/Controllers/Team2unitsreportController.php

652 lines
21 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Http\Requests\LambdaRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class Team2unitsreportController extends Controller
{
private const PARTITION = 'rapport-team2';
private const REPORT_START = 'April 2026';
private const ALL_METRICS = [
'1781749605-cb8ed99b-2f03-4c6a-b3a5-3de66354b7a8' => 'Ciphaler Units',
'1781749537-e6eef0c0-00a8-4828-b24b-a20cf54d2d00' => 'SB Units',
'1784689836-ba81d3f3-124a-42a9-ac75-1a5d9f11d008' => 'ACE Units',
'1781688915-36a926b8-b505-4dd2-867c-a544980a4373' => 'All Units',
];
private const REPORT_TYPES = [
'ciphaler' => [
'label' => 'Ciphaler',
'title' => 'Ciphaler RCPA doctor wise report',
'tag_attributes' => ['po5_ciphaler'],
'metrics' => [
'1781749605-cb8ed99b-2f03-4c6a-b3a5-3de66354b7a8',
'1781688915-36a926b8-b505-4dd2-867c-a544980a4373',
],
],
'sb' => [
'label' => 'Synchrobreathe',
'title' => 'Synchrobreathe RCPA doctor wise report',
'tag_attributes' => ['po5_synchrobreathe'],
'metrics' => [
'1781749537-e6eef0c0-00a8-4828-b24b-a20cf54d2d00',
'1781688915-36a926b8-b505-4dd2-867c-a544980a4373',
],
],
'ace' => [
'label' => 'Ace Doctors',
'title' => 'Ace Doctors RCPA doctor wise report',
'tag_attributes' => ['ace_dr_tagging_report_ace_novel_meet'],
'metrics' => [
'1784689836-ba81d3f3-124a-42a9-ac75-1a5d9f11d008',
'1781688915-36a926b8-b505-4dd2-867c-a544980a4373',
],
],
];
private ?string $accessToken = null;
private ?object $tokenUser = null;
public function __construct()
{
$this->_service = 'reports';
$this->_module = 'contact_metric_monthly';
$this->_viewname = 'team2unitsreport';
$this->_title = 'Ciphaler / SB / Ace Units Report';
$this->_base_url = '/team2unitsreport';
}
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->assertTeam2Access($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);
$type = $this->resolveReportType($request);
$typeLabel = self::REPORT_TYPES[$type]['label'];
$title = self::REPORT_TYPES[$type]['title'];
return $this->view('report')->with([
'title' => $title,
'base_url' => $this->_base_url,
'token' => $token,
'type' => $type,
'typeLabel' => $typeLabel,
'months' => $report->months ?? [],
'metrics' => $report->metrics ?? [],
'doctors' => $doctors,
'data' => $data,
'pagination' => $pagination,
'perPage' => $perPage,
'search' => $search,
]);
}
public function export(Request $request)
{
$this->bootstrapTokenSession($request);
$token = $this->assertTeam2Access($request);
$type = $this->resolveReportType($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.');
}
$title = self::REPORT_TYPES[$type]['title'];
return $this->view('export')->with([
'title' => $title . ' Export',
'base_url' => $this->_base_url,
'export_id' => $exportId,
'search' => $search,
'token' => $token,
'type' => $type,
]);
}
public function exportStatus(Request $request)
{
$token = $this->assertTeam2Access($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 resolveReportPage(object $report, Request $request, int $perPage, string $search = ''): array
{
$doctors = $this->normalizeDoctorsList($report->doctors ?? null);
// Search is applied server-side (tagged-doctor haystack). Do not re-filter the page here.
$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 || (int) $apiPagination->total_pages > 1);
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 resolveReportType(Request $request): string
{
$type = strtolower(trim((string) $request->query('type', 'ciphaler')));
if (!isset(self::REPORT_TYPES[$type])) {
$type = 'ciphaler';
}
return $type;
}
protected function metricsForType(string $type): array
{
$ids = self::REPORT_TYPES[$type]['metrics'] ?? self::REPORT_TYPES['ciphaler']['metrics'];
$metrics = [];
foreach ($ids as $id) {
$metrics[$id] = self::ALL_METRICS[$id] ?? $id;
}
return $metrics;
}
protected function buildReportPayload(Request $request, bool $fetchAll = false, int $perPage = 50, string $search = ''): array
{
$type = $this->resolveReportType($request);
$metrics = $this->metricsForType($type);
$payload = [
'partition' => self::PARTITION,
'metric_ids' => json_encode(array_keys($metrics)),
'metric_labels' => json_encode($metrics),
'start_month' => self::REPORT_START,
'type' => $type,
'tag_attributes' => json_encode(self::REPORT_TYPES[$type]['tag_attributes'] ?? []),
// Tagging columns are used only for doctor filtering, not shown in UI/export.
'include_tagging_columns' => '0',
];
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('team2_units_report_per_page', $perPage);
return $perPage;
}
if ($request->session()->has('team2_units_report_per_page')) {
$perPage = (int) $request->session()->get('team2_units_report_per_page');
if (in_array($perPage, $allowed, true)) {
return $perPage;
}
}
return $default;
}
protected function assertTeam2Access(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->isTeam2User($user)) {
abort(403, 'Access denied. A valid team2 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->isTeam2User($user)) {
$payload = $this->decodeTokenPayload($token);
if (!$this->isTokenPayloadExpired($payload)) {
$this->persistAccessToken($request, $token);
}
}
}
protected function isTeam2User(object $user): bool
{
return ($user->domain ?? '') === 'team2'
|| ($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->isTeam2User($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->isTeam2User($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('team2_units_report_token', $token);
$this->tokenStorage()->put($this->tokenStoragePath($request), $token);
}
protected function loadPersistedToken(Request $request): string
{
if ($request->session()->has('team2_units_report_token')) {
$sessionToken = $this->normalizeRawToken((string) $request->session()->get('team2_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('team2_units_report_token');
$path = $this->tokenStoragePath($request);
if ($this->tokenStorage()->exists($path)) {
$this->tokenStorage()->delete($path);
}
}
protected function tokenStoragePath(Request $request): string
{
return 'team2_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([
'type' => $this->resolveReportType($request),
'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;
}
}