319 lines
12 KiB
PHP
319 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Traits\AccountInfoTrait;
|
|
use App\Models\Transaction;
|
|
use Carbon\Carbon;
|
|
use Barryvdh\DomPDF\Facade\Pdf;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Spatie\Browsershot\Browsershot;
|
|
|
|
class ReportController extends Controller
|
|
{
|
|
use AccountInfoTrait;
|
|
|
|
/**
|
|
* Create a new controller instance.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->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),
|
|
];
|
|
}
|
|
} |