libra/app/Services/MonthlyReportService.php

336 lines
12 KiB
PHP

<?php
namespace App\Services;
use App\Enums\ConsultationStatus;
use App\Enums\ConsultationType;
use App\Enums\PostStatus;
use App\Enums\TimelineStatus;
use App\Enums\UserStatus;
use App\Enums\UserType;
use App\Models\Consultation;
use App\Models\Post;
use App\Models\Timeline;
use App\Models\TimelineUpdate;
use App\Models\User;
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpFoundation\StreamedResponse;
class MonthlyReportService
{
public function generate(int $year, int $month): StreamedResponse
{
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
$endDate = $startDate->copy()->endOfMonth();
$locale = Auth::user()->preferred_language ?? 'en';
$data = [
'period' => $startDate->translatedFormat('F Y'),
'periodMonth' => $startDate->translatedFormat('F'),
'periodYear' => $year,
'generatedAt' => now()->translatedFormat('d M Y H:i'),
'locale' => $locale,
'userStats' => $this->getUserStats($startDate, $endDate),
'consultationStats' => $this->getConsultationStats($startDate, $endDate),
'timelineStats' => $this->getTimelineStats($startDate, $endDate),
'postStats' => $this->getPostStats($startDate, $endDate),
'charts' => $this->renderChartsAsImages($startDate, $endDate, $locale),
'previousMonth' => $this->getPreviousMonthComparison($startDate),
'executiveSummary' => $this->generateExecutiveSummary($startDate, $endDate, $locale),
];
$pdf = Pdf::loadView('pdf.monthly-report', $data)
->setPaper('a4', 'portrait');
$pdf->setOption('isHtml5ParserEnabled', true);
$pdf->setOption('defaultFont', 'DejaVu Sans');
$filename = "monthly-report-{$year}-{$month}.pdf";
return response()->streamDownload(
fn () => print ($pdf->output()),
$filename
);
}
public function getUserStats(Carbon $start, Carbon $end): array
{
$newClients = User::query()
->whereBetween('created_at', [$start, $end])
->whereIn('user_type', [UserType::Individual, UserType::Company])
->count();
$totalActive = User::query()
->where('status', UserStatus::Active)
->where('created_at', '<=', $end)
->whereIn('user_type', [UserType::Individual, UserType::Company])
->count();
$individual = User::query()
->where('user_type', UserType::Individual)
->where('status', UserStatus::Active)
->where('created_at', '<=', $end)
->count();
$company = User::query()
->where('user_type', UserType::Company)
->where('status', UserStatus::Active)
->where('created_at', '<=', $end)
->count();
return [
'new_clients' => $newClients,
'total_active' => $totalActive,
'individual' => $individual,
'company' => $company,
];
}
public function getConsultationStats(Carbon $start, Carbon $end): array
{
$total = Consultation::query()
->whereBetween('booking_date', [$start, $end])
->count();
$completed = Consultation::query()
->whereBetween('booking_date', [$start, $end])
->whereIn('status', [ConsultationStatus::Completed, ConsultationStatus::NoShow])
->count();
$noShows = Consultation::query()
->whereBetween('booking_date', [$start, $end])
->where('status', ConsultationStatus::NoShow)
->count();
$free = Consultation::query()
->whereBetween('booking_date', [$start, $end])
->where('consultation_type', ConsultationType::Free)
->count();
$paid = Consultation::query()
->whereBetween('booking_date', [$start, $end])
->where('consultation_type', ConsultationType::Paid)
->count();
return [
'total' => $total,
'approved' => Consultation::query()
->whereBetween('booking_date', [$start, $end])
->where('status', ConsultationStatus::Approved)
->count(),
'completed' => Consultation::query()
->whereBetween('booking_date', [$start, $end])
->where('status', ConsultationStatus::Completed)
->count(),
'cancelled' => Consultation::query()
->whereBetween('booking_date', [$start, $end])
->where('status', ConsultationStatus::Cancelled)
->count(),
'no_show' => $noShows,
'free' => $free,
'paid' => $paid,
'no_show_rate' => $completed > 0 ? round(($noShows / $completed) * 100, 1) : 0,
];
}
public function getTimelineStats(Carbon $start, Carbon $end): array
{
return [
'active' => Timeline::query()
->where('status', TimelineStatus::Active)
->where('created_at', '<=', $end)
->count(),
'new' => Timeline::query()
->whereBetween('created_at', [$start, $end])
->count(),
'updates' => TimelineUpdate::query()
->whereBetween('created_at', [$start, $end])
->count(),
'archived' => Timeline::query()
->where('status', TimelineStatus::Archived)
->whereBetween('updated_at', [$start, $end])
->count(),
];
}
public function getPostStats(Carbon $start, Carbon $end): array
{
return [
'this_month' => Post::query()
->where('status', PostStatus::Published)
->whereBetween('published_at', [$start, $end])
->count(),
'total' => Post::query()
->where('status', PostStatus::Published)
->where('published_at', '<=', $end)
->count(),
];
}
private function renderChartsAsImages(Carbon $start, Carbon $end, string $locale): array
{
$free = Consultation::query()
->whereBetween('booking_date', [$start, $end])
->where('consultation_type', ConsultationType::Free)
->count();
$paid = Consultation::query()
->whereBetween('booking_date', [$start, $end])
->where('consultation_type', ConsultationType::Paid)
->count();
$consultationPieChart = $this->generateQuickChart([
'type' => 'pie',
'data' => [
'labels' => [
__('report.free', [], $locale),
__('report.paid', [], $locale),
],
'datasets' => [[
'data' => [$free, $paid],
'backgroundColor' => ['#8AB357', '#A5C87A'],
]],
],
'options' => [
'plugins' => [
'legend' => [
'position' => 'bottom',
],
],
],
]);
$trendChart = $this->generateTrendChart($start, $locale);
return [
'consultation_pie' => $consultationPieChart,
'trend_line' => $trendChart,
];
}
private function generateQuickChart(array $config): string
{
$url = 'https://quickchart.io/chart?c='.urlencode(json_encode($config)).'&w=400&h=300&bkg=white';
try {
$response = Http::timeout(10)->get($url);
if ($response->successful()) {
return 'data:image/png;base64,'.base64_encode($response->body());
}
return '';
} catch (\Exception $e) {
return '';
}
}
private function generateTrendChart(Carbon $endMonth, string $locale): string
{
$labels = [];
$data = [];
for ($i = 5; $i >= 0; $i--) {
$month = $endMonth->copy()->subMonths($i);
$labels[] = $month->translatedFormat('M Y');
$data[] = Consultation::query()
->whereMonth('booking_date', $month->month)
->whereYear('booking_date', $month->year)
->count();
}
return $this->generateQuickChart([
'type' => 'line',
'data' => [
'labels' => $labels,
'datasets' => [[
'label' => __('report.consultations', [], $locale),
'data' => $data,
'borderColor' => '#8AB357',
'backgroundColor' => 'rgba(138, 179, 87, 0.1)',
'fill' => true,
'tension' => 0.3,
]],
],
'options' => [
'plugins' => [
'legend' => [
'position' => 'bottom',
],
],
'scales' => [
'y' => [
'beginAtZero' => true,
'ticks' => [
'precision' => 0,
],
],
],
],
]);
}
public function getPreviousMonthComparison(Carbon $currentStart): ?array
{
$prevStart = $currentStart->copy()->subMonth()->startOfMonth();
$prevEnd = $prevStart->copy()->endOfMonth();
$prevConsultations = Consultation::query()
->whereBetween('booking_date', [$prevStart, $prevEnd])
->count();
$prevClients = User::query()
->whereBetween('created_at', [$prevStart, $prevEnd])
->whereIn('user_type', [UserType::Individual, UserType::Company])
->count();
if ($prevConsultations === 0 && $prevClients === 0) {
return null;
}
return [
'consultations' => $prevConsultations,
'clients' => $prevClients,
];
}
private function generateExecutiveSummary(Carbon $start, Carbon $end, string $locale): array
{
$userStats = $this->getUserStats($start, $end);
$consultationStats = $this->getConsultationStats($start, $end);
$previousMonth = $this->getPreviousMonthComparison($start);
$highlights = [];
// New clients highlight
if ($userStats['new_clients'] > 0) {
$highlights[] = __('report.highlight_new_clients', ['count' => $userStats['new_clients']], $locale);
}
// Consultations highlight
if ($consultationStats['total'] > 0) {
$highlights[] = __('report.highlight_consultations', ['count' => $consultationStats['total']], $locale);
}
// Month-over-month comparison
if ($previousMonth) {
$consultationChange = $consultationStats['total'] - $previousMonth['consultations'];
if ($consultationChange > 0) {
$highlights[] = __('report.highlight_growth', ['count' => $consultationChange], $locale);
} elseif ($consultationChange < 0) {
$highlights[] = __('report.highlight_decrease', ['count' => abs($consultationChange)], $locale);
}
}
// No-show rate alert
if ($consultationStats['no_show_rate'] > 20) {
$highlights[] = __('report.highlight_noshow_alert', ['rate' => $consultationStats['no_show_rate']], $locale);
}
return $highlights;
}
}