336 lines
12 KiB
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' => ['#A68966', '#C4A882'],
|
|
]],
|
|
],
|
|
'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' => '#A68966',
|
|
'backgroundColor' => 'rgba(166, 137, 102, 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;
|
|
}
|
|
}
|