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

742 lines
24 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Exports\InvoicesExport;
use App\Http\Requests\LambdaRequest;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
/**
* Titans HCP Gathering Report — MySQL-backed import + token report.
*
* Quick Action (Rapport admin, titans partition):
* Type: external_with_token
* URL: https://<laravel-domain>/titanshcpgatheringreport/report?token=TOKEN
*/
class TitanshcpgatheringreportController extends Controller
{
private const DOMAIN = 'titans';
private const SESSION_TOKEN_KEY = 'titans_hcp_gathering_report_token';
private ?string $accessToken = null;
private ?object $reportUser = null;
public function __construct()
{
$this->_viewname = 'titanshcpgatheringreport';
$this->_title = 'Titans HCP Gathering Report';
$this->_base_url = '/titanshcpgatheringreport';
$this->middleware(function ($request, $next) {
$action = $request->route('action');
if (in_array($action, ['report', 'export'], true)) {
return $next($request);
}
if ($action === null && $request->route()->getName() === 'titanshcpgatheringreport-list' && $request->filled('token')) {
return $next($request);
}
if (!Auth::check()) {
return redirect('/login');
}
return $next($request);
});
}
public function list(Request $request)
{
if ($request->filled('token')) {
return redirect($this->buildUrl('/report', $request));
}
$this->assertTitansAdmin();
$imports = DB::table('titans_hcp_gathering_imports')
->orderByDesc('created_at')
->limit(100)
->get();
return $this->view('list')->with([
'title' => $this->_title,
'base_url' => $this->_base_url,
'imports' => $imports,
'buttons' => $this->getListButtons(),
]);
}
public function import(Request $request)
{
$this->assertTitansAdmin();
return $this->view('import')->with([
'title' => 'Import Titans HCP Data',
'base_url' => $this->_base_url,
]);
}
public function postimport(Request $request)
{
$this->assertTitansAdmin();
try {
$validator = Validator::make($request->all(), [
'report_month' => 'required|date_format:Y-m',
'import_file' => 'required|file|max:10240',
], [
'report_month.required' => 'Please select a report month.',
'report_month.date_format' => 'Report month must be in YYYY-MM format.',
'import_file.required' => 'Please choose a file to upload.',
'import_file.file' => 'The upload did not contain a valid file.',
'import_file.max' => 'The file may not be larger than 10MB.',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput()
->with('error', $validator->errors()->first());
}
$file = $request->file('import_file');
$extension = strtolower((string) $file->getClientOriginalExtension());
if (!in_array($extension, ['xlsx', 'xls', 'csv'], true)) {
return redirect()->back()
->withInput()
->with('error', 'Only .xlsx, .xls, and .csv files are supported.');
}
$reportMonth = $request->input('report_month');
$importId = DB::table('titans_hcp_gathering_imports')->insertGetId([
'file_name' => $file->getClientOriginalName(),
'report_month' => $reportMonth,
'status' => 'processing',
'uploaded_by' => (string) Auth::id(),
'created_at' => now(),
'updated_at' => now(),
]);
$inserted = 0;
$replaced = 0;
$skipped = 0;
$errors = [];
$rows = $this->parseImportFile($file);
if (empty($rows['headers']) || empty($rows['data'])) {
throw new \RuntimeException('The file is empty or has no data rows.');
}
$columnIndex = $this->mapHeaderIndexes($rows['headers']);
if (!isset($columnIndex['pcode']) || !isset($columnIndex['legacy_code'])) {
throw new \RuntimeException('Required columns PCODE and LEGACY_CODE were not found in the file header row.');
}
foreach ($rows['data'] as $rowIndex => $row) {
$lineNo = $rowIndex + 2;
try {
$record = $this->mapRowToRecord($row, $columnIndex);
if (empty($record['pcode']) || empty($record['legacy_code'])) {
$skipped++;
continue;
}
$existing = DB::table('titans_hcp_gathering')
->where('pcode', $record['pcode'])
->where('legacy_code', $record['legacy_code'])
->where('report_month', $reportMonth)
->first();
$payload = array_merge($record, [
'report_month' => $reportMonth,
'import_id' => $importId,
'updated_at' => now(),
]);
if ($existing) {
DB::table('titans_hcp_gathering')->where('id', $existing->id)->update($payload);
$replaced++;
} else {
DB::table('titans_hcp_gathering')->insert(array_merge($payload, [
'created_at' => now(),
]));
$inserted++;
}
} catch (\Throwable $e) {
$errors[] = $this->formatRowError($lineNo, $e);
}
}
$status = empty($errors) ? 'completed' : 'completed_with_errors';
$this->updateImportLog($importId, [
'status' => $status,
'total_rows' => count($rows['data']),
'inserted_count' => $inserted,
'replaced_count' => $replaced,
'skipped_count' => $skipped,
'error_count' => count($errors),
'error_log' => empty($errors) ? null : implode("\n", array_slice($errors, 0, 50)),
'processed_at' => now(),
'updated_at' => now(),
]);
$message = "Import complete. Inserted: {$inserted}, Replaced: {$replaced}, Skipped: {$skipped}.";
if (!empty($errors)) {
$message .= ' Errors: ' . count($errors) . ' (see import log).';
}
if ($inserted === 0 && $replaced === 0) {
$message .= ' No rows were imported. Check that PCODE and LEGACY_CODE columns are present and populated.';
}
return redirect('/titanshcpgatheringreports')->with('message', $message);
} catch (\Throwable $e) {
Log::error('Titans HCP gathering import failed', [
'error' => $e->getMessage(),
'user_id' => Auth::id(),
]);
if (isset($importId)) {
$this->updateImportLog($importId, [
'status' => 'failed',
'error_log' => $e->getMessage(),
'processed_at' => now(),
'updated_at' => now(),
]);
}
return redirect()->back()
->withInput()
->with('error', $this->sanitizeDbString($e->getMessage()) ?: 'Import failed.');
}
}
public function report(Request $request)
{
$token = $this->assertTitansAccess($request);
$month = $this->resolveReportMonth($request);
$months = $this->availableMonths();
$rows = $this->fetchReportRows($month);
$tmVisitLabel = $this->tmVisitColumnLabel($month);
return $this->view('report')->with([
'title' => $this->_title,
'base_url' => $this->_base_url,
'token' => $token,
'month' => $month,
'months' => $months,
'rows' => $rows,
'tm_visit_label' => $tmVisitLabel,
]);
}
public function export(Request $request)
{
$this->assertTitansAccess($request);
$month = $this->resolveReportMonth($request);
$rows = $this->fetchReportRows($month);
$tmVisitLabel = $this->tmVisitColumnLabel($month);
$header = [
'ZBM', 'RBM', 'AREA', 'HQ', 'TM Name', 'HCP NAME',
'LEGACY_CODE', 'PCODE', 'AgreementNo', 'Status', 'EAS ID',
'Activity Report Status', 'Payment Status', 'Closure Done', 'Closure Approved',
$tmVisitLabel,
];
$excelData = [$header];
foreach ($rows as $row) {
$excelData[] = [
$row->zbm,
$row->rbm,
$row->area,
$row->hq,
$row->tm_name,
$row->hcp_name,
$row->legacy_code,
$row->pcode,
$row->agreement_no,
$row->status,
$row->eas_id,
$row->activity_report_status,
$row->payment_status,
$row->closure_done,
$row->closure_approved,
$row->tm_visit,
];
}
$filename = 'titans-hcp-gathering-' . $month . '.xlsx';
return Excel::download(new InvoicesExport($excelData), $filename);
}
protected function getListButtons(): array
{
return [
['title' => 'Import', 'link' => $this->_base_url . '/import'],
];
}
protected function assertTitansAdmin(): void
{
$user = Auth::user();
$permissions = json_decode($user->permissions ?? '[]');
if (!is_array($permissions)) {
$permissions = [];
}
if (($user->domain ?? '') !== self::DOMAIN) {
abort(403);
}
if (!in_array('all_data_access', $permissions, true) && !in_array('formexports_get_all', $permissions, true)) {
abort(403);
}
}
protected function assertTitansAccess(Request $request): string
{
$token = $this->resolveAccessToken($request);
if (!$token) {
abort(401, 'Access token is required');
}
$user = $this->getUserFromToken($token);
if (!$user || !$this->isTitansUser($user)) {
abort(403, 'Invalid access token or user is not in the Titans domain.');
}
$this->accessToken = $token;
$this->reportUser = $user;
return $token;
}
protected function resolveAccessToken(Request $request): ?string
{
$fromRequest = $this->extractRequestToken($request);
if ($fromRequest !== '') {
$user = $this->getUserFromToken($fromRequest);
if ($user && $this->isTitansUser($user)) {
$this->persistAccessToken($request, $fromRequest);
return $fromRequest;
}
}
$storedToken = $this->loadPersistedToken($request);
if ($storedToken !== '') {
$user = $this->getUserFromToken($storedToken);
if ($user && $this->isTitansUser($user)) {
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(self::SESSION_TOKEN_KEY, $token);
}
protected function loadPersistedToken(Request $request): string
{
if (!$request->session()->has(self::SESSION_TOKEN_KEY)) {
return '';
}
return $this->normalizeRawToken((string) $request->session()->get(self::SESSION_TOKEN_KEY));
}
protected function forgetPersistedToken(Request $request): void
{
$request->session()->forget(self::SESSION_TOKEN_KEY);
}
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
{
$payload = $this->decodeTokenPayload($token);
if (!is_object($payload)) {
return null;
}
$user = $payload->user ?? null;
if (!is_object($user)) {
return null;
}
if (!isset($user->permissions) && isset($payload->permissions)) {
$user->permissions = $payload->permissions;
}
if (!isset($user->all_data_access) && isset($payload->all_data_access)) {
$user->all_data_access = $payload->all_data_access;
}
return $user;
}
protected function decodeTokenPayload(string $token): ?object
{
$token = $this->normalizeRawToken($token);
$parts = explode('.', $token);
if (count($parts) < 2 || $parts[1] === '') {
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 isTitansUser(object $user): bool
{
$domain = strtolower((string) ($user->domain ?? ''));
if ($domain === self::DOMAIN) {
return true;
}
$partition = strtolower((string) ($user->partition ?? ''));
return $partition === 'rapport-' . self::DOMAIN;
}
protected function buildUrl(string $path, Request $request): string
{
$query = array_filter([
'token' => $request->input('token') ?: $request->session()->get(self::SESSION_TOKEN_KEY),
'month' => $request->input('month'),
]);
$url = $this->_base_url . $path;
if (!empty($query)) {
$url .= '?' . http_build_query($query);
}
return $url;
}
protected function resolveReportMonth(Request $request): string
{
$month = $request->input('month');
if ($month && preg_match('/^\d{4}-\d{2}$/', $month)) {
return $month;
}
$latest = DB::table('titans_hcp_gathering')->max('report_month');
return $latest ?: Carbon::now()->format('Y-m');
}
protected function availableMonths(): array
{
$query = DB::table('titans_hcp_gathering')
->select('report_month')
->distinct();
$query = $this->applyReportUserScope($query, $this->reportUser);
return $query
->orderByDesc('report_month')
->pluck('report_month')
->all();
}
protected function fetchReportRows(string $month)
{
$query = DB::table('titans_hcp_gathering')
->where('report_month', $month);
$query = $this->applyReportUserScope($query, $this->reportUser);
return $query
->orderBy('hcp_name')
->orderBy('pcode')
->get();
}
protected function userHasAllDataAccess(?object $user): bool
{
if (!$user) {
return false;
}
if (!empty($user->all_data_access)) {
return true;
}
$permissions = $user->permissions ?? '[]';
if (is_string($permissions)) {
$permissions = json_decode($permissions, true);
}
if (!is_array($permissions)) {
$permissions = [];
}
return in_array('all_data_access', $permissions, true);
}
protected function resolveScopedHierarchyIds(?object $user): array
{
if (!$user || empty($user->id)) {
return [];
}
$partition = 'rapport-' . ($user->domain ?? self::DOMAIN);
$hierarchyIds = [];
if (!empty($user->hierarchy)) {
$hierarchyIds[] = $user->hierarchy;
}
try {
$response = LambdaRequest::getrequest('ums', 'hierarchy/hierarchy_below_me', [
'partition' => $partition,
'user_id' => $user->id,
]);
if (isset($response->data->hierarchies) && is_array($response->data->hierarchies)) {
foreach ($response->data->hierarchies as $hierarchy) {
if (!empty($hierarchy->id)) {
$hierarchyIds[] = $hierarchy->id;
}
}
}
} catch (\Throwable $e) {
Log::warning('Titans HCP report hierarchy scope failed', [
'user_id' => $user->id ?? null,
'error' => $e->getMessage(),
]);
}
return array_values(array_unique(array_filter($hierarchyIds)));
}
protected function applyReportUserScope($query, ?object $user)
{
if (!$user || $this->userHasAllDataAccess($user)) {
return $query;
}
$hierarchyIds = $this->resolveScopedHierarchyIds($user);
$userId = (string) ($user->id ?? '');
return $query->where(function ($scopedQuery) use ($hierarchyIds, $userId) {
if (empty($hierarchyIds) && $userId === '') {
$scopedQuery->whereRaw('1 = 0');
return;
}
if (!empty($hierarchyIds)) {
$scopedQuery->whereIn('hierarchy_id', $hierarchyIds);
}
if ($userId !== '') {
$scopedQuery->orWhere('user_id', $userId);
}
});
}
protected function tmVisitColumnLabel(string $month): string
{
try {
return Carbon::createFromFormat('Y-m', $month)->format('F') . ' TM Visit';
} catch (\Throwable $e) {
return 'TM Visit';
}
}
protected function parseImportFile($file): array
{
$extension = strtolower($file->getClientOriginalExtension());
if (in_array($extension, ['xlsx', 'xls'], true)) {
$data = Excel::toArray([], $file);
if (empty($data[0])) {
return ['headers' => [], 'data' => []];
}
$sheet = $data[0];
$headers = array_shift($sheet);
return ['headers' => $headers, 'data' => $sheet];
}
$lines = array_map('str_getcsv', file($file->getRealPath()));
$headers = array_shift($lines) ?: [];
return ['headers' => $headers, 'data' => $lines];
}
protected function normalizeHeader(string $header): string
{
$header = preg_replace('/^\xEF\xBB\xBF/', '', $header);
$header = strtolower(trim(preg_replace('/\s+/', ' ', $header)));
return $header;
}
protected function mapHeaderIndexes(array $headers): array
{
$map = [];
$tmVisitIndex = null;
foreach ($headers as $index => $header) {
$normalized = $this->normalizeHeader((string) $header);
if (str_contains($normalized, 'tm visit')) {
$tmVisitIndex = $index;
continue;
}
$field = $this->headerToField($normalized);
if ($field) {
$map[$field] = $index;
}
}
if ($tmVisitIndex !== null) {
$map['tm_visit'] = $tmVisitIndex;
}
return $map;
}
protected function headerToField(string $normalized): ?string
{
$aliases = [
's.no' => 'sno',
'sno' => 'sno',
's no' => 'sno',
'zbm' => 'zbm',
'rbm' => 'rbm',
'area' => 'area',
'hq' => 'hq',
'tm name' => 'tm_name',
'hcp name' => 'hcp_name',
'doctor_id' => 'doctor_id',
'doctor id' => 'doctor_id',
'hierarchy_id' => 'hierarchy_id',
'hierarchy id' => 'hierarchy_id',
'user_id' => 'user_id',
'user id' => 'user_id',
'legacy_code' => 'legacy_code',
'legacy code' => 'legacy_code',
'pcode' => 'pcode',
'agreementno' => 'agreement_no',
'agreement no' => 'agreement_no',
'status' => 'status',
'eas id' => 'eas_id',
'activity report status' => 'activity_report_status',
'payment status' => 'payment_status',
'closure done' => 'closure_done',
'closure approved' => 'closure_approved',
];
return $aliases[$normalized] ?? null;
}
protected function mapRowToRecord(array $row, array $columnIndex): array
{
$get = function (string $field) use ($row, $columnIndex) {
if (!isset($columnIndex[$field])) {
return null;
}
$value = $row[$columnIndex[$field]] ?? null;
if ($value === null || $value === '') {
return null;
}
return $this->sanitizeDbString((string) $value);
};
$sno = $get('sno');
return [
'sno' => is_numeric($sno) ? (int) $sno : null,
'zbm' => $get('zbm'),
'rbm' => $get('rbm'),
'area' => $get('area'),
'hq' => $get('hq'),
'tm_name' => $get('tm_name'),
'hcp_name' => $get('hcp_name'),
'doctor_id' => $get('doctor_id'),
'hierarchy_id' => $get('hierarchy_id'),
'user_id' => $get('user_id'),
'legacy_code' => $get('legacy_code'),
'pcode' => $get('pcode'),
'agreement_no' => $get('agreement_no'),
'status' => $get('status'),
'eas_id' => $get('eas_id'),
'activity_report_status' => $get('activity_report_status'),
'payment_status' => $get('payment_status'),
'closure_done' => $get('closure_done'),
'closure_approved' => $get('closure_approved'),
'tm_visit' => $get('tm_visit'),
];
}
protected function sanitizeDbString(?string $value): ?string
{
if ($value === null) {
return null;
}
$value = str_replace("\0", '', $value);
if (!mb_check_encoding($value, 'UTF-8')) {
$converted = @iconv('Windows-1252', 'UTF-8//IGNORE', $value);
if ($converted !== false && $converted !== '') {
$value = $converted;
}
}
if (function_exists('iconv')) {
$converted = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
if ($converted !== false) {
$value = $converted;
}
} elseif (function_exists('mb_convert_encoding')) {
$value = mb_convert_encoding($value, 'UTF-8', 'UTF-8');
}
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value) ?? '';
$value = trim($value);
return $value === '' ? null : $value;
}
protected function formatRowError(int $lineNo, \Throwable $e): string
{
$message = $e->getMessage();
if (str_contains($message, 'Incorrect string value')) {
return "Row {$lineNo}: Invalid characters in row data.";
}
$message = $this->sanitizeDbString($message) ?: 'Unknown row error.';
if (strlen($message) > 240) {
$message = substr($message, 0, 240) . '...';
}
return "Row {$lineNo}: {$message}";
}
protected function updateImportLog(int $importId, array $payload): void
{
if (array_key_exists('error_log', $payload)) {
$payload['error_log'] = $payload['error_log'] === null
? null
: $this->sanitizeDbString((string) $payload['error_log']);
}
DB::table('titans_hcp_gathering_imports')->where('id', $importId)->update($payload);
}
}