Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View 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),
];
}
}