From 275789f4d8ea6a694ba1954901c056c380a125a7 Mon Sep 17 00:00:00 2001 From: Lupu Gheorghe Date: Tue, 22 Sep 2026 20:45:54 +0300 Subject: [PATCH 1/2] Match the statistics page to the published design. Co-authored-by: Cursor --- database/seeders/StatisticDatasetSeeder.php | 16 +- lang/en/statistics.php | 50 +++-- lang/ro/statistics.php | 40 +++- .../views/components/embed-card.blade.php | 24 ++- .../components/statistic-chart.blade.php | 176 ++++++++++++++++++ .../\342\232\241statistics-index.blade.php" | 117 ++++++++---- .../\342\232\241statistics-show.blade.php" | 15 +- tests/Feature/StatisticsPageTest.php | 21 +++ 8 files changed, 378 insertions(+), 81 deletions(-) create mode 100644 resources/views/components/statistic-chart.blade.php create mode 100644 tests/Feature/StatisticsPageTest.php diff --git a/database/seeders/StatisticDatasetSeeder.php b/database/seeders/StatisticDatasetSeeder.php index 58dc4d3..a9b11be 100644 --- a/database/seeders/StatisticDatasetSeeder.php +++ b/database/seeders/StatisticDatasetSeeder.php @@ -18,8 +18,8 @@ public function run(): void [ 'slug' => 'cases-per-year', 'chart_type' => ChartType::Bar, - 'title_ro' => 'Evoluția cazurilor de trafic (2019-2024)', - 'title_en' => 'Evolution of trafficking cases (2019-2024)', + 'title_ro' => 'Evoluția cazurilor (2019-2024)', + 'title_en' => 'Case Evolution (2019-2024)', 'description_ro' => 'Cazuri de trafic de copii raportate anual în România.', 'description_en' => 'Child trafficking cases reported annually in Romania.', 'narrative_ro' => 'Datele indică o tendință de creștere, de la 245 de cazuri în 2019 la 423 în 2024.', @@ -29,8 +29,8 @@ public function run(): void [ 'slug' => 'regional-distribution', 'chart_type' => ChartType::Bar, - 'title_ro' => 'Distribuția regională a cazurilor (2024)', - 'title_en' => 'Regional distribution of cases (2024)', + 'title_ro' => 'Cazuri pe regiune (2024)', + 'title_en' => 'Cases by Region (2024)', 'description_ro' => 'Distribuția cazurilor pe regiuni de dezvoltare.', 'description_en' => 'Distribution of cases by development region.', 'narrative_ro' => 'București-Ilfov a înregistrat cele mai multe cazuri, urmată de Nord-Est și Sud-Est.', @@ -40,8 +40,8 @@ public function run(): void [ 'slug' => 'age-distribution', 'chart_type' => ChartType::Pie, - 'title_ro' => 'Distribuția pe grupe de vârstă', - 'title_en' => 'Distribution by age group', + 'title_ro' => 'Distribuție pe grupe de vârstă', + 'title_en' => 'Distribution by Age Group', 'description_ro' => 'Distribuția procentuală a victimelor pe categorii de vârstă.', 'description_en' => 'Percentage distribution of victims by age category.', 'narrative_ro' => 'Grupa 12-14 ani este cea mai vulnerabilă, reprezentând 38% din victimele identificate.', @@ -52,7 +52,7 @@ public function run(): void 'slug' => 'exploitation-types', 'chart_type' => ChartType::Bar, 'title_ro' => 'Tipuri de exploatare', - 'title_en' => 'Types of exploitation', + 'title_en' => 'Types of Exploitation', 'description_ro' => 'Clasificarea cazurilor după forma dominantă de exploatare.', 'description_en' => 'Classification of cases by dominant form of exploitation.', 'narrative_ro' => 'Munca forțată este cea mai frecventă formă, urmată de exploatarea sexuală și cerșetorie.', @@ -63,7 +63,7 @@ public function run(): void 'slug' => 'monthly-trends', 'chart_type' => ChartType::Line, 'title_ro' => 'Tendințe lunare 2024', - 'title_en' => 'Monthly trends 2024', + 'title_en' => 'Monthly Trends 2024', 'description_ro' => 'Evoluția lunară a cazurilor noi și a victimelor identificate.', 'description_en' => 'Monthly evolution of new cases and identified victims.', 'narrative_ro' => 'Ambele serii ating un vârf în luna iulie și scad treptat în a doua jumătate a anului.', diff --git a/lang/en/statistics.php b/lang/en/statistics.php index 7a2e2c1..312c982 100644 --- a/lang/en/statistics.php +++ b/lang/en/statistics.php @@ -1,20 +1,48 @@ 'Statistics and Data — Child Trafficking in Romania', - 'subtitle' => 'Updated data on child trafficking and abuse in Romania.', - 'total_cases' => 'Total cases 2024', - 'victims' => 'Identified victims', - 'conviction' => 'Conviction rate', - 'recovered' => 'Recovered victims', - 'indexes_title' => 'Main monitoring indexes', - 'indexes_subtitle' => 'Three indexes provide a broad picture of trafficking risks and institutional capacity.', + 'title' => 'Statistics and Data - Child Trafficking in Romania', + 'subtitle' => 'Updated data on child trafficking and abuse in Romania. All charts can be embedded on your website.', + 'total_cases' => 'Total Cases 2024', + 'cases_note' => '+6.3% vs 2023', + 'victims' => 'Identified Victims', + 'victims_note' => 'Children under 18', + 'conviction' => 'Conviction Rate', + 'conviction_note' => '+12% vs 2023', + 'recovered' => 'Recovered Victims', + 'recovered_note' => '1,110 children safe', + 'indexes_title' => 'Main Monitoring Indexes', + 'indexes_subtitle' => 'This platform monitors three fundamental indexes that provide a complete picture of child trafficking in Romania', 'indexes' => [ - 'vulnerability' => 'Structural Vulnerability', - 'resilience' => 'Institutional Resilience', - 'rti' => 'Risk of Trafficking Activity', + 'vulnerability' => 'Vulnerabilitate Structurală (V)', + 'vulnerability_subtitle' => 'Structural Vulnerability to Trafficking', + 'vulnerability_trend' => '↓ 1.4 points vs 2023', + 'vulnerability_description' => 'Measures socio-economic factors that increase trafficking risk (poverty, school dropout, unemployment)', + 'resilience' => 'Reziliență Instituțională (R)', + 'resilience_subtitle' => 'Institutional & Community Resilience', + 'resilience_trend' => '↑ 3.3 points vs 2023', + 'resilience_description' => 'Evaluates the capacity of institutions and communities to prevent and combat trafficking', + 'rti' => 'Risc Activitate Trafic (RTI)', + 'rti_subtitle' => 'Risk of Trafficking Activity Index', + 'rti_trend' => '↓ 2.7 points vs 2023', + 'rti_description' => 'Composite index that combines V and R to calculate net trafficking risk', ], 'view_details' => 'View details', + 'charts' => [ + 'cases-per-year' => 'Case Evolution (2019-2024)', + 'regional-distribution' => 'Cases by Region (2024)', + 'age-distribution' => 'Distribution by Age Group', + 'exploitation-types' => 'Types of Exploitation', + 'monthly-trends' => 'Monthly Trends 2024', + ], + 'series' => [ + 'reported' => 'Reported Cases', + 'cases' => 'New Cases', + 'victims' => 'Identified Victims', + ], + 'embed_banner_title' => 'Embed Data on Your Website', + 'embed_banner_body' => 'Each chart and statistic can be embedded on your organization\'s website. Click the "Embed" button in the corner of any card to copy the code.', + 'embed_banner_source' => 'Data is updated monthly and comes from official reports of the Ministry of Internal Affairs, ANITP, and partner organizations.', 'datasets' => 'Datasets', 'none' => 'No data is available yet.', 'back' => 'Back to statistics', diff --git a/lang/ro/statistics.php b/lang/ro/statistics.php index 5d8d94b..9166fc8 100644 --- a/lang/ro/statistics.php +++ b/lang/ro/statistics.php @@ -1,20 +1,48 @@ 'Statistici și date — Traficul de copii în România', - 'subtitle' => 'Date actualizate privind traficul și abuzul de copii în România.', + 'title' => 'Statistici și date - Traficul de copii în România', + 'subtitle' => 'Date actualizate privind traficul și abuzul de copii în România. Toate graficele pot fi integrate pe site-ul dvs.', 'total_cases' => 'Total cazuri 2024', + 'cases_note' => '+6.3% vs 2023', 'victims' => 'Victime identificate', + 'victims_note' => 'Copii sub 18 ani', 'conviction' => 'Rata condamnărilor', + 'conviction_note' => '+12% vs 2023', 'recovered' => 'Victime recuperate', + 'recovered_note' => '1,110 copii în siguranță', 'indexes_title' => 'Principalii indici de monitorizare', - 'indexes_subtitle' => 'Trei indici oferă o imagine amplă a riscurilor și capacității instituționale.', + 'indexes_subtitle' => 'Această platformă monitorizează trei indici fundamentali care oferă o imagine completă a traficului de copii în România', 'indexes' => [ - 'vulnerability' => 'Vulnerabilitate structurală', - 'resilience' => 'Reziliență instituțională', - 'rti' => 'Risc activitate trafic', + 'vulnerability' => 'Vulnerabilitate Structurală (V)', + 'vulnerability_subtitle' => 'Vulnerabilitate structurală la trafic', + 'vulnerability_trend' => '↓ 1,4 puncte față de 2023', + 'vulnerability_description' => 'Măsoară factorii socio-economici care cresc riscul de trafic (sărăcie, abandon școlar, șomaj)', + 'resilience' => 'Reziliență Instituțională (R)', + 'resilience_subtitle' => 'Reziliență instituțională și comunitară', + 'resilience_trend' => '↑ 3,3 puncte față de 2023', + 'resilience_description' => 'Evaluează capacitatea instituțiilor și comunităților de a preveni și combate traficul', + 'rti' => 'Risc Activitate Trafic (RTI)', + 'rti_subtitle' => 'Indicele de risc al activității de trafic', + 'rti_trend' => '↓ 2,7 puncte față de 2023', + 'rti_description' => 'Indice compozit care combină V și R pentru a calcula riscul net de trafic', ], 'view_details' => 'Vezi detalii', + 'charts' => [ + 'cases-per-year' => 'Evoluția cazurilor (2019-2024)', + 'regional-distribution' => 'Cazuri pe regiune (2024)', + 'age-distribution' => 'Distribuție pe grupe de vârstă', + 'exploitation-types' => 'Tipuri de exploatare', + 'monthly-trends' => 'Tendințe lunare 2024', + ], + 'series' => [ + 'reported' => 'Cazuri raportate', + 'cases' => 'Cazuri noi', + 'victims' => 'Victime identificate', + ], + 'embed_banner_title' => 'Integrează date pe site-ul tău', + 'embed_banner_body' => 'Fiecare grafic și statistică poate fi integrată pe site-ul organizației dvs. Faceți clic pe butonul "Embed" din colțul oricărui card pentru a copia codul.', + 'embed_banner_source' => 'Datele sunt actualizate lunar și provin din rapoartele oficiale ale Ministerului Afacerilor Interne, ANITP și organizațiile partenere.', 'datasets' => 'Seturi de date', 'none' => 'Nu există încă date disponibile.', 'back' => 'Înapoi la statistici', diff --git a/resources/views/components/embed-card.blade.php b/resources/views/components/embed-card.blade.php index 1215d5f..16fb54c 100644 --- a/resources/views/components/embed-card.blade.php +++ b/resources/views/components/embed-card.blade.php @@ -1,19 +1,29 @@ @props([ - 'title', + 'title' => null, 'embedCode', + 'floating' => false, ])
merge(['class' => 'relative']) }} x-data="{ showEmbed: false, copied: false }"> - -
-

{{ $title }}

+ + @unless ($floating) +
+

{{ $title }}

+ +
+ @else -
+ @endunless
{{ $slot }} diff --git a/resources/views/components/statistic-chart.blade.php b/resources/views/components/statistic-chart.blade.php new file mode 100644 index 0000000..8df0fe9 --- /dev/null +++ b/resources/views/components/statistic-chart.blade.php @@ -0,0 +1,176 @@ +@props(['dataset']) + +@php + use App\Enums\ChartType; + + $points = $dataset->dataPoints; + $slug = $dataset->slug; + $type = $dataset->chart_type; + $isPie = $type === ChartType::Pie; + $isLine = $type === ChartType::Line; + $isHorizontal = $slug === 'regional-distribution'; + $barColor = match ($slug) { + 'exploitation-types' => '#ef4444', + 'regional-distribution' => '#648baa', + default => '#2a4c82', + }; + $seriesColors = [ + 'cases' => '#2a4c82', + 'victims' => '#648baa', + 'default' => '#2a4c82', + ]; + $ceilings = [ + 'cases-per-year' => 600, + 'regional-distribution' => 160, + 'exploitation-types' => 240, + 'monthly-trends' => 80, + ]; + $maxValue = max(1, (float) $points->max('value')); + $ceiling = ($ceilings[$slug] ?? $maxValue) >= $maxValue ? ($ceilings[$slug] ?? $maxValue) : $maxValue; + $ticks = [0, 0.25, 0.5, 0.75, 1]; + $palette = ['#2a4c82', '#7950a4', '#648baa', '#242159']; +@endphp + +@if ($points->isEmpty()) +

{{ __('statistics.none') }}

+@elseif ($isPie) + @php + $total = max(1, (float) $points->sum('value')); + $showPercent = abs($total - 100) < 1; + $cx = 180; + $cy = 120; + $radius = 72; + $cursor = 0; + @endphp + + @foreach ($points->values() as $index => $point) + @php + $sweep = ($point->value / $total) * 360; + $start = $cursor; + $end = $cursor + $sweep; + $cursor = $end; + $startRad = deg2rad($start - 90); + $endRad = deg2rad($end - 90); + $x1 = $cx + ($radius * cos($startRad)); + $y1 = $cy + ($radius * sin($startRad)); + $x2 = $cx + ($radius * cos($endRad)); + $y2 = $cy + ($radius * sin($endRad)); + $large = $sweep > 180 ? 1 : 0; + $mid = deg2rad((($start + $end) / 2) - 90); + $labelX = $cx + (($radius + 38) * cos($mid)); + $labelY = $cy + (($radius + 38) * sin($mid)); + $color = $point->metadata['color'] ?? $palette[$index % count($palette)]; + $label = $point->label.': '.($showPercent ? (int) $point->value.'%' : number_format($point->value)); + @endphp + + {{ $label }} + @endforeach + +@elseif ($isHorizontal) + @php + $width = 680; + $rowHeight = 32; + $height = 28 + ($points->count() * $rowHeight); + $labelWidth = 176; + $plotLeft = $labelWidth; + $plotWidth = $width - $plotLeft - 16; + @endphp + + @foreach ($ticks as $tick) + @php $x = $plotLeft + ($plotWidth * $tick); @endphp + + {{ (int) round($ceiling * $tick) }} + @endforeach + @foreach ($points->values() as $index => $point) + @php + $y = 12 + ($index * $rowHeight); + $barWidth = ($point->value / $ceiling) * $plotWidth; + @endphp + {{ $point->label }} + + @endforeach + +@else + @php + $width = 640; + $height = $slug === 'exploitation-types' ? 340 : 280; + $plotLeft = 44; + $plotRight = 12; + $plotTop = 12; + $plotBottom = $slug === 'exploitation-types' ? 110 : 52; + $plotWidth = $width - $plotLeft - $plotRight; + $plotHeight = $height - $plotTop - $plotBottom; + $series = $isLine + ? $points->groupBy(fn ($point) => $point->group_key ?? 'default') + : collect(['default' => $points]); + $categories = $series->first()?->values() ?? collect(); + $count = max(1, $categories->count()); + @endphp + + @foreach ($ticks as $tick) + @php + $y = $plotTop + ($plotHeight * (1 - $tick)); + @endphp + + {{ (int) round($ceiling * $tick) }} + @endforeach + + @if ($isLine) + @foreach ($series as $key => $seriesPoints) + @php + $coords = $seriesPoints->values()->map(function ($point, $index) use ($count, $plotLeft, $plotWidth, $plotTop, $plotHeight, $ceiling) { + $x = $plotLeft + (($index + 0.5) * ($plotWidth / $count)); + $y = $plotTop + $plotHeight - (($point->value / $ceiling) * $plotHeight); + + return [$x, $y]; + }); + $color = $seriesColors[$key] ?? '#2a4c82'; + @endphp + + @foreach ($coords as $pair) + + @endforeach + @endforeach + @foreach ($categories as $index => $point) + @php $x = $plotLeft + (($index + 0.5) * ($plotWidth / $count)); @endphp + {{ $point->label }} + @endforeach + @else + @foreach ($categories as $index => $point) + @php + $slot = $plotWidth / $count; + $barWidth = $slot * 0.62; + $x = $plotLeft + ($index * $slot) + (($slot - $barWidth) / 2); + $barHeight = ($point->value / $ceiling) * $plotHeight; + $y = $plotTop + $plotHeight - $barHeight; + $labelX = $x + ($barWidth / 2); + $labelY = $height - 16; + @endphp + + @if ($slug === 'exploitation-types') + {{ $point->label }} + @else + {{ $point->label }} + @endif + @endforeach + @endif + + + @if ($slug === 'cases-per-year' || $isLine) +
+ @if ($isLine) + @foreach ($series as $key => $seriesPoints) + + + {{ __('statistics.series.'.($key === 'default' ? 'reported' : $key)) }} + + @endforeach + @else + + + {{ __('statistics.series.reported') }} + + @endif +
+ @endif +@endif diff --git "a/resources/views/pages/\342\232\241statistics-index.blade.php" "b/resources/views/pages/\342\232\241statistics-index.blade.php" index 5c8dbee..4ba4fc0 100644 --- "a/resources/views/pages/\342\232\241statistics-index.blade.php" +++ "b/resources/views/pages/\342\232\241statistics-index.blade.php" @@ -2,6 +2,7 @@ use App\Models\StatisticDataset; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Facades\Lang; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; use Livewire\Component; @@ -16,57 +17,103 @@ public function datasets(): Collection ->orderBy('sort_order') ->get(); } + + public function chartTitle(StatisticDataset $dataset): string + { + $key = 'statistics.charts.'.$dataset->slug; + + return Lang::has($key) ? __($key) : (string) $dataset->title; + } + + public function embedCode(string $url, string $title): string + { + return ''; + } }; ?>
-
-
- - - - +
+
+ @foreach ([ + ['label' => 'statistics.total_cases', 'value' => '823', 'note' => 'statistics.cases_note', 'valueClass' => 'text-primary', 'icon' => 'alert', 'iconClass' => 'bg-tint-blue text-primary'], + ['label' => 'statistics.victims', 'value' => '1,247', 'note' => 'statistics.victims_note', 'valueClass' => 'text-accent', 'icon' => 'user', 'iconClass' => 'bg-tint-purple text-accent'], + ['label' => 'statistics.conviction', 'value' => '78%', 'note' => 'statistics.conviction_note', 'valueClass' => 'text-muted', 'icon' => 'shield', 'iconClass' => 'bg-tint-blue text-muted'], + ['label' => 'statistics.recovered', 'value' => '89%', 'note' => 'statistics.recovered_note', 'valueClass' => 'text-navy', 'icon' => 'target', 'iconClass' => 'bg-surface-muted text-navy'], + ] as $summary) + +

{{ __($summary['label']) }}

+
+

{{ $summary['value'] }}

+ + @if ($summary['icon'] === 'alert') + + @elseif ($summary['icon'] === 'user') + + @elseif ($summary['icon'] === 'shield') + + @else + + @endif + +
+

{{ __($summary['note']) }}

+
+ @endforeach
-
-

{{ __('statistics.indexes_title') }}

-

{{ __('statistics.indexes_subtitle') }}

-
+
+

{{ __('statistics.indexes_title') }}

+

{{ __('statistics.indexes_subtitle') }}

+
@foreach ([ - ['route' => 'statistics.index-vulnerability', 'key' => 'vulnerability', 'value' => '58.3'], - ['route' => 'statistics.index-resilience', 'key' => 'resilience', 'value' => '67.5'], - ['route' => 'statistics.index-rti', 'key' => 'rti', 'value' => '47.9'], + ['route' => 'statistics.index-vulnerability', 'key' => 'vulnerability', 'value' => '58.3', 'valueClass' => 'text-destructive', 'iconClass' => 'text-destructive'], + ['route' => 'statistics.index-resilience', 'key' => 'resilience', 'value' => '67.5', 'valueClass' => 'text-muted', 'iconClass' => 'text-muted'], + ['route' => 'statistics.index-rti', 'key' => 'rti', 'value' => '47.9', 'valueClass' => 'text-navy', 'iconClass' => 'text-navy'], ] as $index) - -

{{ __("statistics.indexes.{$index['key']}") }}

-

{{ $index['value'] }}

- {{ __('statistics.view_details') }} → +
+
+
+

{{ __("statistics.indexes.{$index['key']}") }}

+

{{ __("statistics.indexes.{$index['key']}_subtitle") }}

+
+ @if ($index['key'] === 'vulnerability') + + @elseif ($index['key'] === 'resilience') + + @else + + @endif +
+

{{ $index['value'] }}

+

{{ __("statistics.indexes.{$index['key']}_trend") }}

+

{{ __("statistics.indexes.{$index['key']}_description") }}

+ {{ __('statistics.view_details') }} →
@endforeach
-
-

{{ __('statistics.datasets') }}

-
diff --git "a/resources/views/pages/\342\232\241statistics-show.blade.php" "b/resources/views/pages/\342\232\241statistics-show.blade.php" index b27e3f7..5c82f35 100644 --- "a/resources/views/pages/\342\232\241statistics-show.blade.php" +++ "b/resources/views/pages/\342\232\241statistics-show.blade.php" @@ -28,20 +28,7 @@ public function mount(string $slug): void - @php($maximum = max(1, (float) $dataset->dataPoints->max('value'))) -
- @foreach ($dataset->dataPoints as $point) -
-
- {{ $point->label }} - {{ number_format($point->value, 2) }} -
-
-
-
-
- @endforeach -
+
@if ($dataset->narrative) diff --git a/tests/Feature/StatisticsPageTest.php b/tests/Feature/StatisticsPageTest.php new file mode 100644 index 0000000..cc488fd --- /dev/null +++ b/tests/Feature/StatisticsPageTest.php @@ -0,0 +1,21 @@ +seed(StatisticDatasetSeeder::class); + + $this->get('/ro/statistics') + ->assertSuccessful() + ->assertSee('Toate graficele pot fi integrate pe site-ul dvs.', false) + ->assertSee('+6.3% vs 2023', false) + ->assertSee('Copii sub 18 ani', false) + ->assertSee('1,110 copii în siguranță', false) + ->assertSee('Vulnerabilitate Structurală (V)', false) + ->assertSee('↓ 1,4 puncte față de 2023', false) + ->assertSee('Integrează date pe site-ul tău', false) + ->assertSee('Evoluția cazurilor (2019-2024)', false) + ->assertSee('Cazuri raportate', false) + ->assertSee('Cazuri noi', false) + ->assertSee('Tendințe lunare 2024', false); +}); From d521e3e9eeb956aa5c165ec7ce82df34417b5ffe Mon Sep 17 00:00:00 2001 From: Lupu Gheorghe Date: Tue, 22 Sep 2026 20:48:55 +0300 Subject: [PATCH 2/2] Expect the national and diaspora counties in the seed data. Co-authored-by: Cursor --- tests/Feature/SeedApplicationDataMigrationTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Feature/SeedApplicationDataMigrationTest.php b/tests/Feature/SeedApplicationDataMigrationTest.php index 3dad03f..3e89933 100644 --- a/tests/Feature/SeedApplicationDataMigrationTest.php +++ b/tests/Feature/SeedApplicationDataMigrationTest.php @@ -9,7 +9,8 @@ uses(RefreshDatabase::class); test('seed application data migration populates production content', function () { - expect(County::query()->count())->toBe(42) + expect(County::query()->count())->toBe(44) + ->and(County::query()->whereIn('code', ['NA', 'DS'])->count())->toBe(2) ->and(Resource::query()->count())->toBeGreaterThan(0) ->and(StaticPage::query()->where('slug', 'terms')->exists())->toBeTrue() ->and(User::query()->where('email', 'admin@cpc.test')->exists())->toBeTrue();