middleware('auth'); } public function reports() { $profile = getActiveProfile(); if (!$profile) { abort(403, 'No active profile'); } // CRITICAL SECURITY: Validate user has ownership/access to this profile // This prevents unauthorized access to financial reports via session manipulation \App\Helpers\ProfileAuthorizationHelper::authorize($profile); $profileAccounts = $this->getAccountsInfo(); return view('reports.show', compact('profileAccounts')); } public function downloadPdf(Request $request) { $profile = getActiveProfile(); if (!$profile) { abort(403, 'No active profile'); } // CRITICAL SECURITY: Validate user has ownership/access to this profile // This prevents unauthorized PDF report generation via session manipulation \App\Helpers\ProfileAuthorizationHelper::authorize($profile); $fromDate = $request->get('fromDate'); $toDate = $request->get('toDate'); $decimalFormat = (bool) $request->get('decimal', 0); if (!$fromDate || !$toDate) { abort(400, 'Missing date parameters'); } // Use the same logic from SingleReport component $singleReport = new \App\Http\Livewire\SingleReport(); $singleReport->fromDate = $fromDate; $singleReport->toDate = $toDate; $singleReport->decimalFormat = $decimalFormat; $accountsData = $singleReport->getAccountsWithPeriodBalances(); $transactionTypesData = $singleReport->calculateTransactionTypeTotals(); $statisticsData = $singleReport->calculatePeriodStatistics(); $returnRatioData = $singleReport->calculateReturnRatioTimeline(); $returnRatioTimelineData = $returnRatioData['timeline'] ?? []; $returnRatioTrendData = $returnRatioData['trend'] ?? []; // Check if we have chart images from the frontend $chartImage = null; // Legacy single chart support $returnRatioChartImage = null; $accountBalancesChartImage = null; $useChartImage = $request->get('with_chart'); // Legacy single chart $useChartsImages = $request->get('with_charts'); // New dual charts Log::info('PDF generation request', [ 'useChartImage' => $useChartImage, 'useChartsImages' => $useChartsImages, 'hasSessionChartImageId' => session()->has('chart_image_id'), 'hasSessionChartImageIds' => session()->has('chart_image_ids'), 'sessionChartImageId' => session('chart_image_id'), 'sessionChartImageIds' => session('chart_image_ids') ]); // Handle new dual charts system if ($useChartsImages && session('chart_image_ids')) { $chartImageIds = session('chart_image_ids'); // Process Return Ratio Chart if (isset($chartImageIds['return_ratio_chart_id'])) { $chartImageId = $chartImageIds['return_ratio_chart_id']; $tempImagePath = storage_path('app/temp/' . $chartImageId . '.png'); if (file_exists($tempImagePath)) { $imageData = file_get_contents($tempImagePath); $returnRatioChartImage = 'data:image/png;base64,' . base64_encode($imageData); Log::info('Return Ratio Chart image found in temp file', [ 'chartImageId' => $chartImageId, 'fileSize' => strlen($imageData) ]); // Clean up temporary file unlink($tempImagePath); } else { Log::warning('Return Ratio Chart image temp file not found', ['path' => $tempImagePath]); } } // Process Account Balances Chart if (isset($chartImageIds['account_balances_chart_id'])) { $chartImageId = $chartImageIds['account_balances_chart_id']; $tempImagePath = storage_path('app/temp/' . $chartImageId . '.png'); if (file_exists($tempImagePath)) { $imageData = file_get_contents($tempImagePath); $accountBalancesChartImage = 'data:image/png;base64,' . base64_encode($imageData); Log::info('Account Balances Chart image found in temp file', [ 'chartImageId' => $chartImageId, 'fileSize' => strlen($imageData) ]); // Clean up temporary file unlink($tempImagePath); } else { Log::warning('Account Balances Chart image temp file not found', ['path' => $tempImagePath]); } } session()->forget('chart_image_ids'); } // Handle legacy single chart system (backward compatibility) else if ($useChartImage && session('chart_image_id')) { $chartImageId = session('chart_image_id'); $tempImagePath = storage_path('app/temp/' . $chartImageId . '.png'); if (file_exists($tempImagePath)) { // Read the image file and convert to base64 $imageData = file_get_contents($tempImagePath); $chartImage = 'data:image/png;base64,' . base64_encode($imageData); Log::info('Legacy chart image found in temp file', [ 'chartImageId' => $chartImageId, 'fileSize' => strlen($imageData), 'base64Length' => strlen($chartImage) ]); // Clean up temporary file unlink($tempImagePath); session()->forget('chart_image_id'); } else { Log::warning('Legacy chart image temp file not found', ['path' => $tempImagePath]); } } else { Log::info('No chart images available, using fallback chart rendering'); } // Prepare chart data for PDF rendering (fallback if no image) $chartData = $this->prepareChartDataForPdf($returnRatioTimelineData, $returnRatioTrendData); // Clean UTF-8 encoding for all data $accountsData = $this->cleanUtf8Data($accountsData); $transactionTypesData = $this->cleanUtf8Data($transactionTypesData); $statisticsData = $this->cleanUtf8Data($statisticsData); // Generate title with profile information $fromDateCarbon = Carbon::parse($fromDate); $toDateCarbon = Carbon::parse($toDate); $activeProfile = getActiveProfile(); $profileTitle = ''; if ($activeProfile) { $fullName = $activeProfile->full_name ?? $activeProfile->name ?? ''; $profileName = $activeProfile->name ?? ''; $profileTitle = ' ' . $fullName . ' (' . $profileName . ')'; } $title = [ 'header' => __('Financial overview') . $profileTitle, 'sub' => $fromDateCarbon->format('d-m-Y') . ' - ' . $toDateCarbon->format('d-m-Y') ]; $pdf = Pdf::loadView('reports.pdf', [ 'title' => $title, 'accountsData' => $accountsData, 'transactionTypesData' => $transactionTypesData, 'statisticsData' => $statisticsData, 'returnRatioTimelineData' => $returnRatioTimelineData, 'returnRatioTrendData' => $returnRatioTrendData, 'chartData' => $chartData, 'chartImage' => $chartImage, // Legacy single chart support 'returnRatioChartImage' => $returnRatioChartImage, 'accountBalancesChartImage' => $accountBalancesChartImage, 'isOrganization' => $activeProfile instanceof \App\Models\Organization, 'decimalFormat' => $decimalFormat, ])->setPaper('A4', 'portrait') ->setOptions([ 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false, 'defaultFont' => 'DejaVu Sans', 'dpi' => 150, 'defaultPaperSize' => 'A4', 'chroot' => public_path(), ]); return $pdf->download('financial-report-' . now()->format('Y-m-d') . '.pdf'); } /** * Clean UTF-8 encoding for strings */ private function cleanUtf8String($string) { if (!is_string($string)) { return $string; } // Remove or replace problematic characters $string = mb_convert_encoding($string, 'UTF-8', 'UTF-8'); $string = preg_replace('/[\x00-\x1F\x7F-\x9F]/u', '', $string); return $string; } /** * Clean UTF-8 encoding for arrays/collections */ private function cleanUtf8Data($data) { if (is_string($data)) { return $this->cleanUtf8String($data); } if (is_array($data) || $data instanceof \Illuminate\Support\Collection) { $cleaned = []; foreach ($data as $key => $value) { $cleanedKey = $this->cleanUtf8String($key); $cleanedValue = $this->cleanUtf8Data($value); $cleaned[$cleanedKey] = $cleanedValue; } return $data instanceof \Illuminate\Support\Collection ? collect($cleaned) : $cleaned; } return $data; } /** * Prepare chart data for PDF rendering */ private function prepareChartDataForPdf($returnRatioTimelineData, $returnRatioTrendData) { if (!$returnRatioTimelineData || count($returnRatioTimelineData) <= 1) { return null; } $values = array_column($returnRatioTimelineData, 'return_ratio'); $labels = array_column($returnRatioTimelineData, 'label'); // Always start from 0% as requested $minValue = 0; $maxValue = count($values) > 0 ? max(array_merge($values, [100])) : 100; // Add padding to top only $padding = ($maxValue - $minValue) * 0.1; $maxValue += $padding; $range = $maxValue - $minValue; // Ensure we have a valid range if ($range <= 0) { $range = 100; $maxValue = 100; } $pointCount = count($returnRatioTimelineData); $points = []; $trendPoints = []; if ($pointCount > 0 && $range > 0) { // Calculate data points foreach ($returnRatioTimelineData as $index => $point) { $x = $pointCount > 1 ? ($index / ($pointCount - 1)) * 100 : 50; $y = 100 - (($point['return_ratio'] - $minValue) / $range * 100); $points[] = number_format($x, 2) . ',' . number_format(max(0, min(100, $y)), 2); } // Calculate trend points if available if ($returnRatioTrendData && count($returnRatioTrendData) > 0) { $trendCount = count($returnRatioTrendData); foreach ($returnRatioTrendData as $index => $point) { $x = $trendCount > 1 ? ($index / ($trendCount - 1)) * 100 : 50; $y = 100 - (($point['trend_value'] - $minValue) / $range * 100); $trendPoints[] = number_format($x, 2) . ',' . number_format(max(0, min(100, $y)), 2); } } } return [ 'values' => $values, 'labels' => $labels, 'minValue' => $minValue, 'maxValue' => $maxValue, 'range' => $range, 'points' => $points, 'trendPoints' => $trendPoints, 'pointsString' => implode(' ', $points), 'trendPointsString' => implode(' ', $trendPoints), ]; } }