Initial commit
This commit is contained in:
319
app/Http/Controllers/ReportController.php
Normal file
319
app/Http/Controllers/ReportController.php
Normal file
@@ -0,0 +1,319 @@
|
||||
<?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),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user