1156 lines
39 KiB
PHP
1156 lines
39 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Exports\IvaStyledExport;
|
|
use App\Http\Requests\LambdaRequest;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Request;
|
|
use Log;
|
|
use Maatwebsite\Excel\Facades\Excel;
|
|
|
|
class IvareportController extends Controller
|
|
{
|
|
private const DOMAIN = 'iva';
|
|
private const PARTITION = 'rapport-iva';
|
|
private const REPORT_START = 'April 2026';
|
|
private const API_TIMEOUT = 120;
|
|
|
|
private const PLANNER_BRANDS = [
|
|
'Materna HMG' => 'Materna HMG',
|
|
'Ciscure' => 'Ciscure',
|
|
'Emprogest' => 'Emprogest',
|
|
];
|
|
|
|
private const KAM_BRANDS = [
|
|
'Materna HMG POB Val' => 'Materna HMG',
|
|
'Materna HCG POB Val' => 'Materna HCG',
|
|
'Materna Depot POB Val' => 'Materna Depot',
|
|
'Ciscure POB Val' => 'Ciscure',
|
|
'Emprogest POB Val' => 'Emprogest',
|
|
'Dydroeva POB Val' => 'Dydroeva',
|
|
'Exhep POB Val' => 'Exhep',
|
|
'Emgra POB Val' => 'Emgra',
|
|
'Silnafil POB Val' => 'Silnafil',
|
|
'Xgrast POB Val' => 'Xgrast',
|
|
'Orofer POB Val' => 'Orofer',
|
|
];
|
|
|
|
private const SKU_BRANDS = [
|
|
'CISCURE' => 'CISCURE',
|
|
'DYDROEVA' => 'DYDROEVA',
|
|
'EMGRA' => 'EMGRA',
|
|
'EMPROGEST' => 'EMPROGEST',
|
|
'EXHEP' => 'EXHEP',
|
|
'MATERNA DEPOT' => 'MATERNA DEPOT',
|
|
'MATERNA HCG' => 'MATERNA HCG',
|
|
'MATERNA' => 'MATERNA',
|
|
'OROFER' => 'OROFER',
|
|
'SILNAFIL' => 'SILNAFIL',
|
|
'XGRAST' => 'XGRAST',
|
|
];
|
|
|
|
private const SKU_GROUPS = [
|
|
'CISCURE' => [
|
|
'CISCURE PFS Rate',
|
|
'CISCURE 0.25 MG INJECTION Rate',
|
|
],
|
|
'DYDROEVA' => [
|
|
'DYDROEVA TABLET 10 MG Rate',
|
|
],
|
|
'EMGRA' => [
|
|
'EMGRA TABLET 10X28T Rate',
|
|
],
|
|
'EMPROGEST' => [
|
|
'EMPROGEST - 200 CAPSULE Rate',
|
|
'EMPROGEST - 400 CAPSULE Rate',
|
|
'EMPROGEST EV 200 TABLET Rate',
|
|
'EMPROGEST GEL 1.125 GM Rate',
|
|
'EMPROGEST SR 200 TABLET Rate',
|
|
'EMPROGEST SR 300 TABLET Rate',
|
|
],
|
|
'EXHEP' => [
|
|
'EXHEP 20 INJECTION Rate',
|
|
'EXHEP 40 MG INJECTION 10PFS Rate',
|
|
'EXHEP 60 MG INJECTION 10PFS Rate',
|
|
'EXHEP PEN CARTRIDGE 300MG/3ML Rate',
|
|
],
|
|
'MATERNA DEPOT' => [
|
|
'MATERNA DEPOT 3.75 MG INJECTION Rate',
|
|
],
|
|
'MATERNA HCG' => [
|
|
'MATERNA-HCG 10000 IU Rate',
|
|
'MATERNA-HCG 5000 IU Rate',
|
|
],
|
|
'MATERNA' => [
|
|
'MATERNA-HMG 150 I.U. Rate',
|
|
'MATERNA-HMG 75 I.U. Rate',
|
|
'MATERNA-HMG HP 1200 IU Rate',
|
|
'MATERNA-HMG HP 225 IU Rate',
|
|
'MATERNA-HMG HP 300 IU Rate',
|
|
],
|
|
'OROFER' => [
|
|
'OROFER S 100 INJECTION Rate',
|
|
'OROFER S 200 INJECTION 10 ML Rate',
|
|
'OROFER S INJECTION 2.5 ML Rate',
|
|
],
|
|
'SILNAFIL' => [
|
|
'SILNAFIL TABLETS Rate',
|
|
],
|
|
'XGRAST' => [
|
|
'XGRAST INJ 300 MCG/0.5 ML PFS Rate',
|
|
],
|
|
];
|
|
|
|
private const COLOR_ID = ['bg' => '00B050', 'fg' => '000000', 'class' => 'iva-th-id'];
|
|
private const COLOR_METRIC = ['bg' => 'FFFF00', 'fg' => '000000', 'class' => 'iva-th-metric'];
|
|
private const COLOR_META = ['bg' => 'E7E6E6', 'fg' => '000000', 'class' => 'iva-th-meta'];
|
|
private const COLOR_SKU = ['bg' => 'FFC000', 'fg' => '000000', 'class' => 'iva-th-sku'];
|
|
|
|
private const SESSION_TOKEN_KEY = 'iva_report_token';
|
|
|
|
private const TOKEN_ACTIONS = [
|
|
'mobile',
|
|
'mobileplanner',
|
|
'mobilekamwise',
|
|
'mobiledrbrandwise',
|
|
'mobileskuwise',
|
|
'export',
|
|
];
|
|
|
|
private ?string $accessToken = null;
|
|
|
|
private ?object $tokenUser = null;
|
|
|
|
private const PLANNER_BRAND_COLORS = [
|
|
'Materna HMG' => ['bg' => 'E7E6E6', 'fg' => '000000'],
|
|
'Ciscure' => ['bg' => 'F8CBAD', 'fg' => '000000'],
|
|
'Emprogest' => ['bg' => 'DEEBF7', 'fg' => '000000'],
|
|
];
|
|
|
|
private const SKU_BRAND_COLORS = [
|
|
'CISCURE' => ['bg' => 'ED7D31', 'fg' => 'FFFFFF'],
|
|
'DYDROEVA' => ['bg' => '3D4C5F', 'fg' => 'FFFFFF'],
|
|
'EMGRA' => ['bg' => 'B4C7E7', 'fg' => '000000'],
|
|
'EMPROGEST' => ['bg' => '548235', 'fg' => 'FFFFFF'],
|
|
'EXHEP' => ['bg' => 'DEEBF7', 'fg' => '000000'],
|
|
'MATERNA DEPOT' => ['bg' => 'FFC000', 'fg' => '000000'],
|
|
'MATERNA HCG' => ['bg' => 'FFE699', 'fg' => '000000'],
|
|
'MATERNA' => ['bg' => 'E2EFDA', 'fg' => '000000'],
|
|
'OROFER' => ['bg' => 'BDD7EE', 'fg' => '000000'],
|
|
'SILNAFIL' => ['bg' => 'B25E25', 'fg' => 'FFFFFF'],
|
|
'XGRAST' => ['bg' => 'A9CE91', 'fg' => '000000'],
|
|
];
|
|
|
|
public function __construct()
|
|
{
|
|
// Routes are /ivareport/{action} -> action(), so except() cannot key off mobile* methods.
|
|
$this->middleware(function ($request, $next) {
|
|
$action = (string) $request->route('action', '');
|
|
if (in_array($action, self::TOKEN_ACTIONS, true)) {
|
|
return $next($request);
|
|
}
|
|
|
|
if (!auth()->check()) {
|
|
if ($request->expectsJson()) {
|
|
abort(401);
|
|
}
|
|
return redirect()->guest(route('login'));
|
|
}
|
|
|
|
if ((auth()->user()->domain ?? '') !== self::DOMAIN) {
|
|
abort(403);
|
|
}
|
|
|
|
return $next($request);
|
|
});
|
|
$this->_service = 'reports';
|
|
$this->_module = 'iva_reports';
|
|
$this->_viewname = 'ivareport';
|
|
$this->_title = 'Custom Dashboards';
|
|
$this->_base_url = '/ivareport';
|
|
}
|
|
|
|
public function list(Request $request)
|
|
{
|
|
$query = $request->getQueryString();
|
|
return redirect('/ivareport/planner' . ($query ? '?' . $query : ''));
|
|
}
|
|
|
|
public function planner(Request $request)
|
|
{
|
|
return $this->renderReport($request, 'planner_data', 'planner', 'Planner Data');
|
|
}
|
|
|
|
public function kamwise(Request $request)
|
|
{
|
|
return $this->renderReport($request, 'kam_wise', 'kamwise', 'KAM wise details');
|
|
}
|
|
|
|
public function drbrandwise(Request $request)
|
|
{
|
|
return $this->renderReport($request, 'dr_and_brandwise', 'drbrandwise', 'Dr & Brand Wise details');
|
|
}
|
|
|
|
public function skuwise(Request $request)
|
|
{
|
|
return $this->renderReport($request, 'sku_wise', 'skuwise', 'SKU wise Rate');
|
|
}
|
|
|
|
public function mobile(Request $request)
|
|
{
|
|
$this->assertIvaToken($request);
|
|
$token = $this->accessToken ?? '';
|
|
$shared = array_filter([
|
|
'authorization' => $token,
|
|
'month' => $request->input('month'),
|
|
'filter_hier' => $request->input('filter_hier', []),
|
|
'selected_brand' => $request->input('selected_brand', []),
|
|
], function ($value) {
|
|
return $value !== null && $value !== '' && $value !== [];
|
|
});
|
|
$query = $shared ? '?' . http_build_query($shared) : '';
|
|
|
|
return view('ivareport.mobile', [
|
|
'title' => 'Custom Dashboards',
|
|
'reports' => [
|
|
['label' => 'Planner Data', 'url' => '/ivareport/mobileplanner' . $query],
|
|
['label' => 'KAM wise details', 'url' => '/ivareport/mobilekamwise' . $query],
|
|
['label' => 'Dr & Brand Wise details', 'url' => '/ivareport/mobiledrbrandwise' . $query],
|
|
['label' => 'SKU wise Rate', 'url' => '/ivareport/mobileskuwise' . $query],
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function mobileplanner(Request $request)
|
|
{
|
|
$this->assertIvaToken($request);
|
|
return $this->renderReport($request, 'planner_data', 'planner', 'Planner Data', true);
|
|
}
|
|
|
|
public function mobilekamwise(Request $request)
|
|
{
|
|
$this->assertIvaToken($request);
|
|
return $this->renderReport($request, 'kam_wise', 'kamwise', 'KAM wise details', true);
|
|
}
|
|
|
|
public function mobiledrbrandwise(Request $request)
|
|
{
|
|
$this->assertIvaToken($request);
|
|
return $this->renderReport($request, 'dr_and_brandwise', 'drbrandwise', 'Dr & Brand Wise details', true);
|
|
}
|
|
|
|
public function mobileskuwise(Request $request)
|
|
{
|
|
$this->assertIvaToken($request);
|
|
return $this->renderReport($request, 'sku_wise', 'skuwise', 'SKU wise Rate', true);
|
|
}
|
|
|
|
public function export(Request $request)
|
|
{
|
|
$this->assertWebOrToken($request);
|
|
$typeMap = [
|
|
'planner' => ['api' => 'planner_data', 'file' => 'planner-data'],
|
|
'kamwise' => ['api' => 'kam_wise', 'file' => 'kam-wise-details'],
|
|
'drbrandwise' => ['api' => 'dr_and_brandwise', 'file' => 'dr-brand-wise-details'],
|
|
'skuwise' => ['api' => 'sku_wise', 'file' => 'sku-wise-rate'],
|
|
];
|
|
$type = $request->input('type', 'planner');
|
|
if (!isset($typeMap[$type])) {
|
|
abort(404);
|
|
}
|
|
|
|
$reportType = $typeMap[$type]['api'];
|
|
$filters = $this->resolveFilters($request, $reportType);
|
|
$payload = $this->fetchReport(
|
|
$reportType,
|
|
$filters['month'],
|
|
$filters['selected_brands'],
|
|
$filters['hierarchy_ids']
|
|
);
|
|
|
|
if ($type === 'planner') {
|
|
$table = $this->buildPlannerTable($payload['report'], $payload['metric_config'], $filters['selected_brands']);
|
|
$export = $this->buildPlannerExport($table);
|
|
} elseif ($type === 'kamwise') {
|
|
$table = $this->buildKamTable($payload['report'], $payload['metric_config'], $filters['selected_brands']);
|
|
$export = $this->buildKamExport($table);
|
|
} elseif ($type === 'drbrandwise') {
|
|
$table = $this->buildDrBrandTable($payload['report'], $filters['selected_brands']);
|
|
$export = $this->buildDrBrandExport($table);
|
|
} else {
|
|
$table = $this->buildSkuTable($payload['report'], $filters['selected_brands']);
|
|
$export = $this->buildSkuExport($table);
|
|
}
|
|
|
|
$monthSlug = preg_replace('/[^A-Za-z0-9]+/', '-', $filters['month']);
|
|
return Excel::download(new IvaStyledExport($export), $typeMap[$type]['file'] . '-' . $monthSlug . '.xlsx');
|
|
}
|
|
|
|
private function renderReport(Request $request, string $reportType, string $view, string $title, bool $isMobile = false)
|
|
{
|
|
$filters = $this->resolveFilters($request, $reportType);
|
|
$payload = $this->fetchReport(
|
|
$reportType,
|
|
$filters['month'],
|
|
$filters['selected_brands'],
|
|
$filters['hierarchy_ids']
|
|
);
|
|
$token = $isMobile ? ($this->accessToken ?? '') : null;
|
|
$filterQuery = $this->filterQuery($filters, $token);
|
|
|
|
$viewData = [
|
|
'title' => $title,
|
|
'activeTab' => $view,
|
|
'isMobile' => $isMobile,
|
|
'token' => $token ?? '',
|
|
'formAction' => $isMobile ? $this->mobileReportPath($view) : '',
|
|
'mobileBackUrl' => $isMobile ? '/ivareport/mobile' . ($filterQuery ? '?' . $filterQuery : '') : '',
|
|
'months' => $this->availableMonths(),
|
|
'selectedMonth' => $filters['month'],
|
|
'filter_hier' => $this->loadHierarchies($isMobile ? $this->tokenUser : null),
|
|
'selectedHier' => $filters['hierarchy_ids'],
|
|
'brandOptions' => $this->brandOptions($reportType),
|
|
'selectedBrands' => $filters['selected_brands'],
|
|
'filterQuery' => $filterQuery,
|
|
'error' => $payload['error'],
|
|
'rows' => [],
|
|
'ivaFmt' => function ($val, $isPct = false) {
|
|
return self::formatNumber($val, $isPct);
|
|
},
|
|
'ivaMetricsTitle' => function ($metrics) {
|
|
return self::metricsTitle($metrics);
|
|
},
|
|
];
|
|
|
|
if ($reportType === 'planner_data') {
|
|
$viewData = array_merge($viewData, $this->buildPlannerTable($payload['report'], $payload['metric_config'], $filters['selected_brands']));
|
|
} elseif ($reportType === 'kam_wise') {
|
|
$viewData = array_merge($viewData, $this->buildKamTable($payload['report'], $payload['metric_config'], $filters['selected_brands']));
|
|
} elseif ($reportType === 'dr_and_brandwise') {
|
|
$viewData = array_merge($viewData, $this->buildDrBrandTable($payload['report'], $filters['selected_brands']));
|
|
} else {
|
|
$viewData = array_merge($viewData, $this->buildSkuTable($payload['report'], $filters['selected_brands']));
|
|
}
|
|
|
|
return view($this->_viewname . '.' . $view, $viewData);
|
|
}
|
|
|
|
private function resolveFilters(Request $request, string $reportType): array
|
|
{
|
|
$month = trim((string) $request->input('month', Carbon::now()->format('F Y')));
|
|
$validMonths = $this->availableMonths();
|
|
$validValues = array_column($validMonths, 'value');
|
|
if (!in_array($month, $validValues, true)) {
|
|
$month = Carbon::now()->format('F Y');
|
|
}
|
|
|
|
$hierarchyIds = array_values(array_filter((array) $request->input('filter_hier', [])));
|
|
$allowedBrands = array_keys($this->brandOptions($reportType));
|
|
$selectedBrands = array_values(array_intersect(
|
|
array_filter((array) $request->input('selected_brand', [])),
|
|
$allowedBrands
|
|
));
|
|
|
|
return [
|
|
'month' => $month,
|
|
'hierarchy_ids' => $hierarchyIds,
|
|
'selected_brands' => $selectedBrands,
|
|
];
|
|
}
|
|
|
|
private function availableMonths(): array
|
|
{
|
|
$months = [['value' => 'overall', 'label' => 'Overall']];
|
|
try {
|
|
$start = Carbon::createFromFormat('F Y', self::REPORT_START)->startOfMonth();
|
|
} catch (\Exception $e) {
|
|
$start = Carbon::now()->startOfMonth();
|
|
}
|
|
$end = Carbon::now()->startOfMonth();
|
|
if ($start->gt($end)) {
|
|
$start = $end->copy();
|
|
}
|
|
$cursor = $end->copy();
|
|
while ($cursor->gte($start)) {
|
|
$label = $cursor->format('F Y');
|
|
$months[] = ['value' => $label, 'label' => $label];
|
|
$cursor->subMonth();
|
|
}
|
|
return $months;
|
|
}
|
|
|
|
private function brandOptions(string $reportType): array
|
|
{
|
|
if ($reportType === 'planner_data') {
|
|
return self::PLANNER_BRANDS;
|
|
}
|
|
if ($reportType === 'sku_wise') {
|
|
return self::SKU_BRANDS;
|
|
}
|
|
return self::KAM_BRANDS;
|
|
}
|
|
|
|
private function filterQuery(array $filters, ?string $token = null): string
|
|
{
|
|
$query = [
|
|
'month' => $filters['month'],
|
|
'filter_hier' => $filters['hierarchy_ids'],
|
|
'selected_brand' => $filters['selected_brands'],
|
|
];
|
|
if ($token) {
|
|
$query['authorization'] = $token;
|
|
}
|
|
|
|
return http_build_query($query);
|
|
}
|
|
|
|
private function mobileReportPath(string $view): string
|
|
{
|
|
return '/ivareport/mobile' . $view;
|
|
}
|
|
|
|
private function assertWebOrToken(Request $request): void
|
|
{
|
|
$user = auth()->user();
|
|
if ($user) {
|
|
if (($user->domain ?? '') !== self::DOMAIN) {
|
|
abort(403);
|
|
}
|
|
return;
|
|
}
|
|
|
|
$this->assertIvaToken($request);
|
|
}
|
|
|
|
private function assertIvaToken(Request $request): void
|
|
{
|
|
$token = $this->resolveAccessToken($request);
|
|
if ($token === null) {
|
|
abort(401, 'Access token is required');
|
|
}
|
|
|
|
$user = $this->getUserFromToken($token);
|
|
if (!$user || ($user->domain ?? '') !== self::DOMAIN) {
|
|
abort(403);
|
|
}
|
|
|
|
$this->accessToken = $token;
|
|
$this->tokenUser = $user;
|
|
}
|
|
|
|
private function resolveAccessToken(Request $request): ?string
|
|
{
|
|
$candidates = [
|
|
$request->query('authorization'),
|
|
$request->query('token'),
|
|
$request->header('Authorization'),
|
|
];
|
|
$token = '';
|
|
foreach ($candidates as $candidate) {
|
|
$candidate = trim((string) ($candidate ?? ''));
|
|
if (stripos($candidate, 'Bearer ') === 0) {
|
|
$candidate = trim(substr($candidate, 7));
|
|
}
|
|
if ($candidate !== '') {
|
|
$token = $candidate;
|
|
break;
|
|
}
|
|
}
|
|
if ($token === '' && $request->session()->has(self::SESSION_TOKEN_KEY)) {
|
|
$token = (string) $request->session()->get(self::SESSION_TOKEN_KEY);
|
|
}
|
|
|
|
if ($token !== '') {
|
|
$request->session()->put(self::SESSION_TOKEN_KEY, $token);
|
|
}
|
|
|
|
return $token !== '' ? $token : null;
|
|
}
|
|
|
|
private function getUserFromToken(string $token): ?object
|
|
{
|
|
$parts = explode('.', $token);
|
|
if (count($parts) < 2) {
|
|
return null;
|
|
}
|
|
|
|
$payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')));
|
|
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;
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
private function apiGet(string $service, string $url, array $data, int $timeout = 30)
|
|
{
|
|
if ($this->accessToken) {
|
|
return LambdaRequest::getrequest($service, $url, $data, $this->accessToken, $timeout);
|
|
}
|
|
|
|
return LambdaRequest::signedrequest('get', $service, $url, $data, $timeout);
|
|
}
|
|
|
|
private function fetchReport(string $reportType, string $month, array $brands, array $hierIds): array
|
|
{
|
|
try {
|
|
$response = $this->apiGet(
|
|
'reports',
|
|
'iva_reports/brand_reports',
|
|
[
|
|
'report_type' => $reportType,
|
|
'selected_brand' => json_encode(array_values($brands)),
|
|
'month' => $month,
|
|
'hierarchy_ids' => json_encode(array_values($hierIds)),
|
|
],
|
|
self::API_TIMEOUT
|
|
);
|
|
} catch (\Throwable $e) {
|
|
Log::error('IVA custom dashboard request failed', [
|
|
'report_type' => $reportType,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
return ['report' => [], 'metric_config' => [], 'error' => 'Unable to load report. Please try again.'];
|
|
}
|
|
|
|
if (!$response || (($response->status ?? '') !== 'success')) {
|
|
$message = $response->message ?? 'Unable to load report. Please try again.';
|
|
Log::warning('IVA custom dashboard API error', [
|
|
'report_type' => $reportType,
|
|
'message' => $message,
|
|
]);
|
|
return ['report' => [], 'metric_config' => [], 'error' => $message];
|
|
}
|
|
|
|
$data = $response->data ?? null;
|
|
$report = [];
|
|
$metricConfig = [];
|
|
if ($data) {
|
|
$report = json_decode(json_encode($data->report ?? []), true) ?: [];
|
|
$metricConfig = json_decode(json_encode($data->metric_config ?? []), true) ?: [];
|
|
}
|
|
|
|
return ['report' => $report, 'metric_config' => $metricConfig, 'error' => null];
|
|
}
|
|
|
|
private function loadHierarchies(?object $user = null): array
|
|
{
|
|
$user = $user ?? auth()->user();
|
|
$permissions = $user->permissions ?? '[]';
|
|
if (is_string($permissions)) {
|
|
$permissions = json_decode($permissions, true);
|
|
} elseif (is_object($permissions)) {
|
|
$permissions = json_decode(json_encode($permissions), true);
|
|
}
|
|
if (!is_array($permissions)) {
|
|
$permissions = [];
|
|
}
|
|
$hierarchyId = in_array('all_data_access', $permissions, true) ? 'root' : [];
|
|
|
|
try {
|
|
$response = $this->apiGet(
|
|
'ums',
|
|
'hierarchy/hierarchy_below_me',
|
|
[
|
|
'partition' => self::PARTITION,
|
|
'hierarchy_id' => $hierarchyId,
|
|
]
|
|
);
|
|
} catch (\Throwable $e) {
|
|
Log::error('IVA hierarchy load failed', ['error' => $e->getMessage()]);
|
|
return [];
|
|
}
|
|
|
|
$hierarchies = $response->data->hierarchies ?? [];
|
|
if (!is_array($hierarchies)) {
|
|
$hierarchies = [];
|
|
}
|
|
usort($hierarchies, function ($a, $b) {
|
|
return ((int) ($b->level ?? 0)) <=> ((int) ($a->level ?? 0));
|
|
});
|
|
|
|
return $hierarchies;
|
|
}
|
|
|
|
private function visibleBrandKeys(array $all, array $selected): array
|
|
{
|
|
if (empty($selected)) {
|
|
return array_keys($all);
|
|
}
|
|
return array_values(array_intersect(array_keys($all), $selected));
|
|
}
|
|
|
|
private function buildPlannerTable(array $report, array $metricConfig, array $selectedBrands): array
|
|
{
|
|
$brandKeys = $this->visibleBrandKeys(self::PLANNER_BRANDS, $selectedBrands);
|
|
$brandGroups = [];
|
|
foreach ($brandKeys as $brand) {
|
|
$brandGroups[] = [
|
|
'name' => $brand,
|
|
'style' => $this->brandStyle(self::PLANNER_BRAND_COLORS, $brand, 'iva-planner-'),
|
|
'columns' => [
|
|
['key' => 'plan', 'label' => 'POB Plan (Val)'],
|
|
['key' => 'done', 'label' => 'POB Done (Val)'],
|
|
['key' => 'pct', 'label' => '% POB'],
|
|
],
|
|
];
|
|
}
|
|
|
|
$rows = [];
|
|
foreach ($report as $item) {
|
|
$item = (array) $item;
|
|
$row = [
|
|
'territory' => $item['territory'] ?? '',
|
|
'kam_emp_code' => $item['kam_emp_code'] ?? '',
|
|
'kam_name' => $item['kam_name'] ?? '',
|
|
'brands' => [],
|
|
];
|
|
foreach ($brandKeys as $brand) {
|
|
$brandData = (array) ($item[$brand] ?? []);
|
|
$metrics = (array) ($brandData['metrics'] ?? []);
|
|
$row['brands'][$brand] = [
|
|
'plan' => $brandData['POB Plan (Val)'] ?? 0,
|
|
'done' => $brandData['POB Done (Val)'] ?? 0,
|
|
'pct' => $brandData['% POB'] ?? 0,
|
|
'plan_metrics' => $this->normalizeMetrics((array) ($metrics['POB Plan (Val)'] ?? [])),
|
|
'done_metrics' => $this->normalizeMetrics((array) ($metrics['POB Done (Val)'] ?? [])),
|
|
];
|
|
}
|
|
$rows[] = $row;
|
|
}
|
|
|
|
return [
|
|
'brandGroups' => $brandGroups,
|
|
'rows' => $rows,
|
|
'skuColumns' => $this->plannerSkuColumns($metricConfig, $brandKeys, $rows),
|
|
];
|
|
}
|
|
|
|
private function plannerSkuColumns(array $metricConfig, array $brandKeys, array $rows): array
|
|
{
|
|
$byBrand = [];
|
|
foreach ($metricConfig as $entry) {
|
|
$entry = (array) $entry;
|
|
foreach ($entry as $brand => $cfg) {
|
|
if (!in_array($brand, $brandKeys, true)) {
|
|
continue;
|
|
}
|
|
$cfg = (array) $cfg;
|
|
$byBrand[$brand] = $this->skuColumnsFromPlanDone(
|
|
(array) ($cfg['POB Plan (Val)'] ?? []),
|
|
(array) ($cfg['POB Done (Val)'] ?? [])
|
|
);
|
|
}
|
|
}
|
|
|
|
foreach ($brandKeys as $brand) {
|
|
if (!empty($byBrand[$brand])) {
|
|
continue;
|
|
}
|
|
$planMetrics = [];
|
|
$doneMetrics = [];
|
|
foreach ($rows as $row) {
|
|
$planMetrics = array_merge($planMetrics, $row['brands'][$brand]['plan_metrics'] ?? []);
|
|
$doneMetrics = array_merge($doneMetrics, $row['brands'][$brand]['done_metrics'] ?? []);
|
|
}
|
|
$byBrand[$brand] = $this->skuColumnsFromPlanDone($planMetrics, $doneMetrics);
|
|
}
|
|
|
|
return $byBrand;
|
|
}
|
|
|
|
private function skuColumnsFromPlanDone(array $plans, array $dones): array
|
|
{
|
|
$planBySku = [];
|
|
foreach ($plans as $item) {
|
|
$item = (array) $item;
|
|
$attr = $item['attribute'] ?? '';
|
|
if ($attr === '') {
|
|
continue;
|
|
}
|
|
$sku = preg_replace('/ POB Plan Val$/', '', $attr);
|
|
if (!isset($planBySku[$sku])) {
|
|
$planBySku[$sku] = $attr;
|
|
}
|
|
}
|
|
|
|
$doneBySku = [];
|
|
foreach ($dones as $item) {
|
|
$item = (array) $item;
|
|
$attr = $item['attribute'] ?? '';
|
|
if ($attr === '') {
|
|
continue;
|
|
}
|
|
$sku = preg_replace('/ POB Done Val$/', '', $attr);
|
|
if (!isset($doneBySku[$sku])) {
|
|
$doneBySku[$sku] = $attr;
|
|
}
|
|
}
|
|
|
|
$skus = array_unique(array_merge(array_keys($planBySku), array_keys($doneBySku)));
|
|
$columns = [];
|
|
foreach ($skus as $sku) {
|
|
$columns[] = [
|
|
'sku' => $sku,
|
|
'plan_attr' => $planBySku[$sku] ?? ($sku . ' POB Plan Val'),
|
|
'done_attr' => $doneBySku[$sku] ?? ($sku . ' POB Done Val'),
|
|
'pct_label' => $sku . ' POB %',
|
|
];
|
|
}
|
|
|
|
return $columns;
|
|
}
|
|
|
|
private function buildPlannerExport(array $table): array
|
|
{
|
|
$brandGroups = $table['brandGroups'] ?? [];
|
|
$skuColumns = $table['skuColumns'] ?? [];
|
|
$rows = $table['rows'] ?? [];
|
|
|
|
$row1 = ['Territory', 'KAM Emp Code', 'KAM Name'];
|
|
$row2 = ['', '', ''];
|
|
$merges = ['A1:A2', 'B1:B2', 'C1:C2'];
|
|
$fills = [
|
|
'A1:C2' => self::COLOR_ID,
|
|
];
|
|
|
|
$col = 4;
|
|
foreach ($brandGroups as $group) {
|
|
$brand = $group['name'];
|
|
$skuCount = count($skuColumns[$brand] ?? []);
|
|
$span = 3 + ($skuCount * 3);
|
|
$start = $this->excelCol($col);
|
|
$end = $this->excelCol($col + $span - 1);
|
|
$merges[] = $start . '1:' . $end . '1';
|
|
$style = $group['style'] ?? $this->brandStyle(self::PLANNER_BRAND_COLORS, $brand);
|
|
$fills[$start . '1:' . $end . '1'] = $style;
|
|
$fills[$start . '2:' . $end . '2'] = self::COLOR_METRIC;
|
|
|
|
$row1[] = $brand;
|
|
for ($i = 1; $i < $span; $i++) {
|
|
$row1[] = '';
|
|
}
|
|
$row2[] = 'POB Plan (Val)';
|
|
$row2[] = 'POB Done (Val)';
|
|
$row2[] = '% POB';
|
|
foreach ($skuColumns[$brand] ?? [] as $skuCol) {
|
|
$row2[] = $skuCol['plan_attr'];
|
|
$row2[] = $skuCol['done_attr'];
|
|
$row2[] = $skuCol['pct_label'];
|
|
}
|
|
$col += $span;
|
|
}
|
|
|
|
$excelData = [$row1, $row2];
|
|
foreach ($rows as $row) {
|
|
$line = [
|
|
$row['territory'] ?? '',
|
|
$row['kam_emp_code'] ?? '',
|
|
$row['kam_name'] ?? '',
|
|
];
|
|
foreach ($brandGroups as $group) {
|
|
$brand = $group['name'];
|
|
$brandData = $row['brands'][$brand] ?? [];
|
|
$line[] = $this->excelNumber($brandData['plan'] ?? 0);
|
|
$line[] = $this->excelNumber($brandData['done'] ?? 0);
|
|
$line[] = $this->excelNumber($brandData['pct'] ?? 0, true);
|
|
|
|
$planMap = $this->metricsByAttribute($brandData['plan_metrics'] ?? []);
|
|
$doneMap = $this->metricsByAttribute($brandData['done_metrics'] ?? []);
|
|
foreach ($skuColumns[$brand] ?? [] as $skuCol) {
|
|
$planVal = $planMap[$skuCol['plan_attr']] ?? 0;
|
|
$doneVal = $doneMap[$skuCol['done_attr']] ?? 0;
|
|
$pct = ($planVal > 0) ? round(($doneVal / $planVal) * 100, 2) : 0;
|
|
$line[] = $this->excelNumber($planVal);
|
|
$line[] = $this->excelNumber($doneVal);
|
|
$line[] = $this->excelNumber($pct, true);
|
|
}
|
|
}
|
|
$excelData[] = $line;
|
|
}
|
|
|
|
return [
|
|
'data' => $excelData,
|
|
'merges' => $merges,
|
|
'fills' => $fills,
|
|
'header_rows' => 2,
|
|
'freeze' => 'D3',
|
|
];
|
|
}
|
|
|
|
private function brandStyle(array $map, string $name, string $prefix = 'iva-brand-'): array
|
|
{
|
|
$style = $map[$name] ?? ['bg' => 'D9E2F3', 'fg' => '000000'];
|
|
$slug = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $name), '-'));
|
|
$style['class'] = $prefix . $slug;
|
|
return $style;
|
|
}
|
|
|
|
private function excelCol(int $index): string
|
|
{
|
|
$letter = '';
|
|
while ($index > 0) {
|
|
$index--;
|
|
$letter = chr(65 + ($index % 26)) . $letter;
|
|
$index = intdiv($index, 26);
|
|
}
|
|
return $letter;
|
|
}
|
|
|
|
private function metricsByAttribute(array $metrics): array
|
|
{
|
|
$map = [];
|
|
foreach ($metrics as $metric) {
|
|
$attr = $metric['attribute'] ?? '';
|
|
if ($attr === '') {
|
|
continue;
|
|
}
|
|
$map[$attr] = $metric['units'] ?? 0;
|
|
}
|
|
return $map;
|
|
}
|
|
|
|
private function excelNumber($val, $isPct = false)
|
|
{
|
|
if ($val === null || $val === '' || !is_numeric($val)) {
|
|
return $val ?? 0;
|
|
}
|
|
$num = (float) $val;
|
|
if ($isPct) {
|
|
return round($num, 2);
|
|
}
|
|
return $num == floor($num) ? (int) $num : round($num, 2);
|
|
}
|
|
|
|
private function buildKamExport(array $table): array
|
|
{
|
|
$brandColumns = $table['brandColumns'] ?? [];
|
|
$skuColumns = $table['skuColumns'] ?? [];
|
|
$rows = $table['rows'] ?? [];
|
|
|
|
$headers = ['Territory', 'KAM Emp Code', 'KAM Name', 'Total Brand POB Val'];
|
|
$fills = [
|
|
'A1:C1' => self::COLOR_ID,
|
|
'D1:D1' => self::COLOR_METRIC,
|
|
];
|
|
$col = 5;
|
|
foreach ($brandColumns as $brandCol) {
|
|
$key = $brandCol['key'];
|
|
$headers[] = $key;
|
|
$fills[$this->excelCol($col) . '1'] = self::COLOR_METRIC;
|
|
$col++;
|
|
foreach ($skuColumns[$key] ?? [] as $attr) {
|
|
$headers[] = $attr;
|
|
$fills[$this->excelCol($col) . '1'] = self::COLOR_SKU;
|
|
$col++;
|
|
}
|
|
}
|
|
|
|
$excelData = [$headers];
|
|
foreach ($rows as $row) {
|
|
$line = [
|
|
$row['territory'] ?? '',
|
|
$row['kam_emp_code'] ?? '',
|
|
$row['kam_name'] ?? '',
|
|
$this->excelNumber($row['total'] ?? 0),
|
|
];
|
|
foreach ($brandColumns as $brandCol) {
|
|
$key = $brandCol['key'];
|
|
$line[] = $this->excelNumber($row['values'][$key] ?? 0);
|
|
$metricMap = $this->metricsByAttribute($row['metrics'][$key] ?? []);
|
|
foreach ($skuColumns[$key] ?? [] as $attr) {
|
|
$line[] = $this->excelNumber($metricMap[$attr] ?? 0);
|
|
}
|
|
}
|
|
$excelData[] = $line;
|
|
}
|
|
|
|
return [
|
|
'data' => $excelData,
|
|
'merges' => [],
|
|
'fills' => $fills,
|
|
'header_rows' => 1,
|
|
'freeze' => 'D2',
|
|
];
|
|
}
|
|
|
|
private function buildDrBrandExport(array $table): array
|
|
{
|
|
$brandColumns = $table['brandColumns'] ?? [];
|
|
$rows = $table['rows'] ?? [];
|
|
|
|
$headers = ['Dr Name', 'Dr UID', 'Dr SBU', 'Total Brand POB Val'];
|
|
foreach ($brandColumns as $col) {
|
|
$headers[] = $col['key'];
|
|
}
|
|
|
|
$lastCol = $this->excelCol(count($headers));
|
|
$fills = [
|
|
'A1:A1' => self::COLOR_ID,
|
|
'B1:C1' => self::COLOR_META,
|
|
'D1:' . $lastCol . '1' => self::COLOR_METRIC,
|
|
];
|
|
|
|
$excelData = [$headers];
|
|
foreach ($rows as $row) {
|
|
$line = [
|
|
$row['dr_name'] ?? '',
|
|
$row['dr_uid'] ?? '',
|
|
$row['dr_sbu'] ?? '',
|
|
$this->excelNumber($row['total'] ?? 0),
|
|
];
|
|
foreach ($brandColumns as $col) {
|
|
$line[] = $this->excelNumber($row['values'][$col['key']] ?? 0);
|
|
}
|
|
$excelData[] = $line;
|
|
}
|
|
|
|
return [
|
|
'data' => $excelData,
|
|
'merges' => [],
|
|
'fills' => $fills,
|
|
'header_rows' => 1,
|
|
'freeze' => 'B2',
|
|
];
|
|
}
|
|
|
|
private function buildSkuExport(array $table): array
|
|
{
|
|
$groups = $table['skuGroups'] ?? [];
|
|
$rows = $table['rows'] ?? [];
|
|
|
|
$row1 = ['Dr Name'];
|
|
$row2 = [''];
|
|
$merges = ['A1:A2'];
|
|
$fills = [
|
|
'A1:A2' => self::COLOR_ID,
|
|
];
|
|
|
|
$col = 2;
|
|
foreach ($groups as $group) {
|
|
$span = max(1, count($group['columns'] ?? []));
|
|
$start = $this->excelCol($col);
|
|
$end = $this->excelCol($col + $span - 1);
|
|
$merges[] = $start . '1:' . $end . '1';
|
|
$fills[$start . '1:' . $end . '1'] = $group['style'] ?? self::COLOR_METRIC;
|
|
$fills[$start . '2:' . $end . '2'] = self::COLOR_METRIC;
|
|
|
|
$row1[] = $group['name'];
|
|
for ($i = 1; $i < $span; $i++) {
|
|
$row1[] = '';
|
|
}
|
|
foreach ($group['columns'] as $skuCol) {
|
|
$row2[] = $skuCol['key'];
|
|
}
|
|
$col += $span;
|
|
}
|
|
|
|
$excelData = [$row1, $row2];
|
|
foreach ($rows as $row) {
|
|
$line = [$row['dr_name'] ?? ''];
|
|
foreach ($groups as $group) {
|
|
foreach ($group['columns'] as $skuCol) {
|
|
$line[] = $this->excelNumber($row['values'][$skuCol['key']] ?? 0, true);
|
|
}
|
|
}
|
|
$excelData[] = $line;
|
|
}
|
|
|
|
return [
|
|
'data' => $excelData,
|
|
'merges' => $merges,
|
|
'fills' => $fills,
|
|
'header_rows' => 2,
|
|
'freeze' => 'B3',
|
|
];
|
|
}
|
|
|
|
private function buildKamTable(array $report, array $metricConfig, array $selectedBrands): array
|
|
{
|
|
$brandKeys = $this->visibleBrandKeys(self::KAM_BRANDS, $selectedBrands);
|
|
$attrToColumn = [];
|
|
foreach ($metricConfig as $cfg) {
|
|
$cfg = (array) $cfg;
|
|
$attr = $cfg['attribute'] ?? '';
|
|
$column = $cfg['column'] ?? '';
|
|
if ($attr !== '' && $column !== '') {
|
|
$attrToColumn[$attr] = $column;
|
|
}
|
|
}
|
|
|
|
$skuColumns = [];
|
|
foreach ($brandKeys as $key) {
|
|
$skuColumns[$key] = [];
|
|
}
|
|
foreach ($metricConfig as $cfg) {
|
|
$cfg = (array) $cfg;
|
|
$attr = $cfg['attribute'] ?? '';
|
|
$column = $cfg['column'] ?? '';
|
|
if ($attr === '' || !in_array($column, $brandKeys, true)) {
|
|
continue;
|
|
}
|
|
if (!in_array($attr, $skuColumns[$column], true)) {
|
|
$skuColumns[$column][] = $attr;
|
|
}
|
|
}
|
|
|
|
$columns = [];
|
|
foreach ($brandKeys as $key) {
|
|
$columns[] = ['key' => $key, 'label' => self::KAM_BRANDS[$key]];
|
|
}
|
|
|
|
$rows = [];
|
|
foreach ($report as $item) {
|
|
$item = (array) $item;
|
|
$metricsByColumn = [];
|
|
foreach ((array) ($item['metrics'] ?? []) as $metric) {
|
|
$metric = (array) $metric;
|
|
$column = $attrToColumn[$metric['attribute'] ?? ''] ?? '';
|
|
if ($column === '') {
|
|
continue;
|
|
}
|
|
$metricsByColumn[$column][] = [
|
|
'attribute' => $metric['attribute'] ?? '',
|
|
'units' => $metric['units'] ?? 0,
|
|
];
|
|
}
|
|
$values = [];
|
|
foreach ($brandKeys as $key) {
|
|
$values[$key] = $item[$key] ?? 0;
|
|
}
|
|
$rows[] = [
|
|
'territory' => $item['territory'] ?? '',
|
|
'kam_emp_code' => $item['kam_emp_code'] ?? '',
|
|
'kam_name' => $item['kam_name'] ?? '',
|
|
'total' => $item['Total Brand POB Val'] ?? 0,
|
|
'values' => $values,
|
|
'metrics' => $metricsByColumn,
|
|
];
|
|
}
|
|
|
|
return ['brandColumns' => $columns, 'rows' => $rows, 'skuColumns' => $skuColumns];
|
|
}
|
|
|
|
private function buildDrBrandTable(array $report, array $selectedBrands): array
|
|
{
|
|
$brandKeys = $this->visibleBrandKeys(self::KAM_BRANDS, $selectedBrands);
|
|
$columns = [];
|
|
foreach ($brandKeys as $key) {
|
|
$columns[] = ['key' => $key, 'label' => self::KAM_BRANDS[$key]];
|
|
}
|
|
|
|
$rows = [];
|
|
foreach ($report as $item) {
|
|
$item = (array) $item;
|
|
$values = [];
|
|
foreach ($brandKeys as $key) {
|
|
$values[$key] = $item[$key] ?? 0;
|
|
}
|
|
$rows[] = [
|
|
'dr_name' => $item['Dr Name'] ?? '',
|
|
'dr_uid' => $item['Dr UID'] ?? '',
|
|
'dr_sbu' => $item['Dr SBU'] ?? '',
|
|
'total' => $item['Total Brand POB Val'] ?? 0,
|
|
'values' => $values,
|
|
];
|
|
}
|
|
|
|
return ['brandColumns' => $columns, 'rows' => $rows];
|
|
}
|
|
|
|
private function buildSkuTable(array $report, array $selectedBrands): array
|
|
{
|
|
$brandKeys = $this->visibleBrandKeys(self::SKU_BRANDS, $selectedBrands);
|
|
$groups = [];
|
|
foreach ($brandKeys as $brand) {
|
|
$columns = [];
|
|
foreach (self::SKU_GROUPS[$brand] as $column) {
|
|
$columns[] = [
|
|
'key' => $column,
|
|
'label' => preg_replace('/ Rate$/', '', $column),
|
|
];
|
|
}
|
|
$groups[] = [
|
|
'name' => $brand,
|
|
'style' => $this->brandStyle(self::SKU_BRAND_COLORS, $brand, 'iva-sku-'),
|
|
'columns' => $columns,
|
|
];
|
|
}
|
|
|
|
$rows = [];
|
|
foreach ($report as $item) {
|
|
$item = (array) $item;
|
|
$values = [];
|
|
foreach ($groups as $group) {
|
|
foreach ($group['columns'] as $col) {
|
|
$values[$col['key']] = $item[$col['key']] ?? 0;
|
|
}
|
|
}
|
|
$rows[] = [
|
|
'dr_name' => $item['Dr Name'] ?? '',
|
|
'values' => $values,
|
|
];
|
|
}
|
|
|
|
return ['skuGroups' => $groups, 'rows' => $rows];
|
|
}
|
|
|
|
private function normalizeMetrics(array $metrics): array
|
|
{
|
|
$normalized = [];
|
|
foreach ($metrics as $metric) {
|
|
$metric = (array) $metric;
|
|
$normalized[] = [
|
|
'attribute' => $metric['attribute'] ?? '',
|
|
'units' => $metric['units'] ?? 0,
|
|
];
|
|
}
|
|
return $normalized;
|
|
}
|
|
|
|
private static function formatNumber($val, $isPct = false): string
|
|
{
|
|
if ($val === null || $val === '') {
|
|
return '';
|
|
}
|
|
if (!is_numeric($val)) {
|
|
return (string) $val;
|
|
}
|
|
$num = (float) $val;
|
|
if ($isPct) {
|
|
if ($num == 0.0) {
|
|
return '0';
|
|
}
|
|
return number_format($num, 2);
|
|
}
|
|
if (floor($num) == $num) {
|
|
return number_format($num);
|
|
}
|
|
return number_format($num, 2);
|
|
}
|
|
|
|
private static function metricsTitle($metrics): string
|
|
{
|
|
if (empty($metrics) || !is_array($metrics)) {
|
|
return '';
|
|
}
|
|
$lines = [];
|
|
foreach ($metrics as $metric) {
|
|
$metric = (array) $metric;
|
|
$attr = $metric['attribute'] ?? '';
|
|
$units = $metric['units'] ?? 0;
|
|
$lines[] = trim($attr . ': ' . self::formatNumber($units));
|
|
}
|
|
return implode("\n", $lines);
|
|
}
|
|
}
|