'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); } }