From 1b9f63c6f0ead678be83ab8b226ea82caff6b8c2 Mon Sep 17 00:00:00 2001
From: Christian Fiebrig <fiebrig@bitidee.de>
Date: Tue, 28 Jan 2025 18:52:55 +0100
Subject: [PATCH 1/3] 178: create ui for statistics

---
 .../campaigns/screens/campaigns_screen.dart   |   2 +-
 .../campaigns/screens/statistics_screen.dart  | 136 +++++++++++++++++-
 lib/i18n/app_de.json                          |  12 +-
 3 files changed, 142 insertions(+), 8 deletions(-)

diff --git a/lib/features/campaigns/screens/campaigns_screen.dart b/lib/features/campaigns/screens/campaigns_screen.dart
index 94e333e3..3bacade4 100644
--- a/lib/features/campaigns/screens/campaigns_screen.dart
+++ b/lib/features/campaigns/screens/campaigns_screen.dart
@@ -19,7 +19,7 @@ class _CampaignsScreen extends State<CampaignsScreen> with SingleTickerProviderS
     CampaignMenuModel(t.campaigns.poster.label, true, PostersScreen()),
     CampaignMenuModel(t.campaigns.flyer.label, true, FlyerScreen()),
     CampaignMenuModel(t.campaigns.team.label, false, TeamsScreen()),
-    CampaignMenuModel(t.campaigns.statistic.label, false, StatisticsScreen()),
+    CampaignMenuModel(t.campaigns.statistic.label, true, StatisticsScreen()),
   ];
   late TabController _tabController;
 
diff --git a/lib/features/campaigns/screens/statistics_screen.dart b/lib/features/campaigns/screens/statistics_screen.dart
index 83ecc3cb..55e07184 100644
--- a/lib/features/campaigns/screens/statistics_screen.dart
+++ b/lib/features/campaigns/screens/statistics_screen.dart
@@ -1,20 +1,144 @@
+import 'dart:math';
+
 import 'package:flutter/material.dart';
 import 'package:gruene_app/app/theme/theme.dart';
 import 'package:gruene_app/i18n/translations.g.dart';
+import 'package:intl/intl.dart';
 
 class StatisticsScreen extends StatelessWidget {
   const StatisticsScreen({super.key});
 
   @override
   Widget build(BuildContext context) {
-    return Placeholder(
-      color: Colors.red,
-      child: Center(
-        child: Text(
-          t.campaigns.statistic.label,
-          style: TextStyle(fontSize: 20, color: ThemeColors.primary),
+    final theme = Theme.of(context);
+
+    return SingleChildScrollView(
+      child: Container(
+        padding: EdgeInsets.all(16),
+        color: theme.colorScheme.surfaceDim,
+        child: Column(
+          children: [
+            _getBadgeBox(context, theme),
+            SizedBox(height: 12),
+            _getCategoryBox(
+              theme: theme,
+              title: t.campaigns.statistic.recorded_doors,
+            ),
+            SizedBox(height: 12),
+            _getCategoryBox(
+              theme: theme,
+              title: t.campaigns.statistic.recorded_posters,
+              subTitle: t.campaigns.statistic.including_damaged_or_taken_down,
+            ),
+            SizedBox(height: 12),
+            _getCategoryBox(
+              theme: theme,
+              title: t.campaigns.statistic.recorded_flyer,
+            ),
+            Align(
+              alignment: Alignment.centerLeft,
+              child: Text(
+                'Stand: ${DateTime.now().toString()} (${t.campaigns.statistic.update_info})',
+                style: theme.textTheme.labelMedium!.apply(color: ThemeColors.textDisabled),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _getBadgeBox(BuildContext context, ThemeData theme) {
+    var mediaQuery = MediaQuery.of(context);
+    return Container(
+      padding: EdgeInsets.all(16),
+      width: mediaQuery.size.width,
+      decoration: BoxDecoration(
+        color: ThemeColors.primary,
+        borderRadius: BorderRadius.circular(19),
+        boxShadow: [
+          BoxShadow(color: ThemeColors.textDark.withAlpha(10), offset: Offset(2, 4)),
+        ],
+      ),
+      child: Column(
+        children: [
+          Align(
+            alignment: Alignment.centerLeft,
+            child: Text(
+              t.campaigns.statistic.my_badges,
+              style: theme.textTheme.titleMedium!.copyWith(color: theme.colorScheme.surface),
+            ),
+          ),
+          ..._getBadges(),
+        ],
+      ),
+    );
+  }
+
+  List<Widget> _getBadges() {
+    return <Widget>[];
+  }
+
+  Widget _getCategoryBox({required String title, String? subTitle, required ThemeData theme}) {
+    var categoryDecoration = BoxDecoration(
+      color: ThemeColors.background,
+      borderRadius: BorderRadius.circular(19),
+      boxShadow: [
+        BoxShadow(color: ThemeColors.textDark.withAlpha(10), offset: Offset(2, 4)),
+      ],
+    );
+    var rng = Random();
+    return Container(
+      padding: EdgeInsets.all(16),
+      decoration: categoryDecoration,
+      child: Column(
+        children: [
+          Row(
+            children: [
+              Text(
+                title,
+                style: theme.textTheme.titleMedium,
+              ),
+            ],
+          ),
+          subTitle != null
+              ? Row(
+                  children: [
+                    Text(
+                      subTitle,
+                      style: theme.textTheme.labelSmall!.copyWith(color: ThemeColors.textDisabled),
+                    ),
+                  ],
+                )
+              : SizedBox(),
+          _getDataRow(t.campaigns.statistic.by_me, rng.nextInt(200), theme),
+          _getDataRow(t.campaigns.statistic.by_my_KV, rng.nextInt(2000), theme),
+          _getDataRow(t.campaigns.statistic.by_my_LV, rng.nextInt(20000), theme),
+          _getDataRow(t.campaigns.statistic.in_germany, rng.nextInt(20000), theme),
+        ],
+      ),
+    );
+  }
+
+  Widget _getDataRow(String key, int value, ThemeData theme) {
+    var formatter = NumberFormat('#,##,##0', t.$meta.locale.languageCode);
+    return Container(
+      padding: const EdgeInsets.all(4),
+      decoration: BoxDecoration(
+        border: Border(
+          bottom: BorderSide(color: ThemeColors.textLight),
         ),
       ),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Text(key, style: theme.textTheme.labelLarge!.copyWith(color: ThemeColors.textDark)),
+          Text(
+            formatter.format(value),
+            style: theme.textTheme.labelLarge!.copyWith(color: ThemeColors.textDark),
+          ),
+        ],
+      ),
     );
   }
 }
diff --git a/lib/i18n/app_de.json b/lib/i18n/app_de.json
index 7cf0db93..c8abbdf6 100644
--- a/lib/i18n/app_de.json
+++ b/lib/i18n/app_de.json
@@ -111,7 +111,17 @@
       "label": "Team"
     },
     "statistic": {
-      "label": "Statistik"
+      "label": "Statistik",
+      "my_badges": "Meine Abzeichen",
+      "recorded_posters": "Plakate",
+      "including_damaged_or_taken_down": "inkl. beschädigt und abgehängt",
+      "recorded_doors": "Haustüren",
+      "recorded_flyer": "Flyer",
+      "by_me": "von mir erfasst",
+      "by_my_KV": "von meinen Kreisverband",
+      "by_my_LV": "von meinem Landesverband",
+      "in_germany": "deutschlandweit",
+      "update_info": "wird alle 5 Minuten aktualisiert"
     },
     "address": {
       "street": "Straße",

From 448045c39ca74e4b792bf834d80a4d4fdece89da Mon Sep 17 00:00:00 2001
From: Christian Fiebrig <fiebrig@bitidee.de>
Date: Tue, 28 Jan 2025 18:53:15 +0100
Subject: [PATCH 2/3] 314: add ui for badges

---
 assets/badges/badge_bronze.svg                | 132 +++++
 assets/badges/badge_empty.svg                 |   3 +
 assets/badges/badge_gold.svg                  | 178 +++++++
 assets/badges/badge_platinum.svg              | 178 +++++++
 assets/badges/badge_silver.svg                | 178 +++++++
 ...uene_api_campaigns_statistics_service.dart |  59 +++
 lib/app/theme/theme.dart                      |   2 +-
 .../campaigns/screens/statistics_screen.dart  | 168 +++++-
 lib/i18n/app_de.json                          |   1 +
 lib/main.dart                                 |   2 +
 pubspec.yaml                                  |   1 +
 swaggers/gruene-api.yaml                      | 481 +++++++++++++++++-
 12 files changed, 1361 insertions(+), 22 deletions(-)
 create mode 100644 assets/badges/badge_bronze.svg
 create mode 100644 assets/badges/badge_empty.svg
 create mode 100644 assets/badges/badge_gold.svg
 create mode 100644 assets/badges/badge_platinum.svg
 create mode 100644 assets/badges/badge_silver.svg
 create mode 100644 lib/app/services/gruene_api_campaigns_statistics_service.dart

diff --git a/assets/badges/badge_bronze.svg b/assets/badges/badge_bronze.svg
new file mode 100644
index 00000000..e953ac01
--- /dev/null
+++ b/assets/badges/badge_bronze.svg
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="46.737301"
+   height="46.0728"
+   viewBox="0 0 46.737301 46.0728"
+   fill="none"
+   version="1.1"
+   id="svg10"
+   sodipodi:docname="badge_bronze.svg"
+   inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview10"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="8.4719101"
+     inkscape:cx="27.797745"
+     inkscape:cy="19.240053"
+     inkscape:window-width="1920"
+     inkscape:window-height="992"
+     inkscape:window-x="2392"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg10" />
+  <g
+     filter="url(#filter1_i_8602_13905)"
+     id="g2"
+     transform="translate(-16.1313,-15.7944)">
+    <path
+       d="m 39.1697,17.2371 c 0.3962,-0.5903 1.2644,-0.5903 1.6606,0 l 4.4267,6.5954 c 0.2424,0.3611 0.6904,0.5242 1.1082,0.4034 l 7.6304,-2.207 c 0.683,-0.1975 1.3481,0.3606 1.2722,1.0674 l -0.8484,7.8978 c -0.0465,0.4325 0.1919,0.8453 0.5896,1.0213 l 7.2639,3.2142 c 0.6501,0.2876 0.8009,1.1427 0.2884,1.6354 l -5.7266,5.5047 c -0.3135,0.3014 -0.3963,0.7709 -0.2047,1.1613 l 3.4984,7.1314 c 0.3131,0.6382 -0.121,1.3902 -0.8303,1.4381 l -7.9251,0.5359 c -0.434,0.0294 -0.7992,0.3358 -0.9034,0.7581 l -1.904,7.7116 c -0.1704,0.6902 -0.9863,0.9872 -1.5605,0.568 l -6.4155,-4.6836 c -0.3512,-0.2565 -0.828,-0.2565 -1.1792,0 l -6.4155,4.6836 c -0.5742,0.4192 -1.3901,0.1222 -1.5605,-0.568 l -1.904,-7.7116 C 29.4262,52.9722 29.061,52.6658 28.627,52.6364 L 20.7019,52.1005 C 19.9926,52.0526 19.5585,51.3006 19.8716,50.6624 L 23.37,43.531 c 0.1916,-0.3904 0.1088,-0.8599 -0.2047,-1.1613 L 17.4387,36.865 c -0.5125,-0.4927 -0.3617,-1.3478 0.2884,-1.6354 l 7.2639,-3.2142 c 0.3977,-0.176 0.6361,-0.5888 0.5896,-1.0213 l -0.8484,-7.8978 c -0.0759,-0.7068 0.5892,-1.2649 1.2722,-1.0674 l 7.6304,2.207 c 0.4178,0.1208 0.8658,-0.0423 1.1082,-0.4034 z"
+       fill="url(#paint0_radial_8602_13905)"
+       id="path1"
+       style="fill:url(#paint0_radial_8602_13905)" />
+    <path
+       d="m 39.1697,17.2371 c 0.3962,-0.5903 1.2644,-0.5903 1.6606,0 l 4.4267,6.5954 c 0.2424,0.3611 0.6904,0.5242 1.1082,0.4034 l 7.6304,-2.207 c 0.683,-0.1975 1.3481,0.3606 1.2722,1.0674 l -0.8484,7.8978 c -0.0465,0.4325 0.1919,0.8453 0.5896,1.0213 l 7.2639,3.2142 c 0.6501,0.2876 0.8009,1.1427 0.2884,1.6354 l -5.7266,5.5047 c -0.3135,0.3014 -0.3963,0.7709 -0.2047,1.1613 l 3.4984,7.1314 c 0.3131,0.6382 -0.121,1.3902 -0.8303,1.4381 l -7.9251,0.5359 c -0.434,0.0294 -0.7992,0.3358 -0.9034,0.7581 l -1.904,7.7116 c -0.1704,0.6902 -0.9863,0.9872 -1.5605,0.568 l -6.4155,-4.6836 c -0.3512,-0.2565 -0.828,-0.2565 -1.1792,0 l -6.4155,4.6836 c -0.5742,0.4192 -1.3901,0.1222 -1.5605,-0.568 l -1.904,-7.7116 C 29.4262,52.9722 29.061,52.6658 28.627,52.6364 L 20.7019,52.1005 C 19.9926,52.0526 19.5585,51.3006 19.8716,50.6624 L 23.37,43.531 c 0.1916,-0.3904 0.1088,-0.8599 -0.2047,-1.1613 L 17.4387,36.865 c -0.5125,-0.4927 -0.3617,-1.3478 0.2884,-1.6354 l 7.2639,-3.2142 c 0.3977,-0.176 0.6361,-0.5888 0.5896,-1.0213 l -0.8484,-7.8978 c -0.0759,-0.7068 0.5892,-1.2649 1.2722,-1.0674 l 7.6304,2.207 c 0.4178,0.1208 0.8658,-0.0423 1.1082,-0.4034 z"
+       fill="url(#paint1_angular_8602_13905)"
+       fill-opacity="0.1"
+       id="path2"
+       style="fill:url(#paint1_angular_8602_13905)" />
+  </g>
+  <defs
+     id="defs10">
+    <filter
+       id="filter1_i_8602_13905"
+       x="16.1313"
+       y="15.7944"
+       width="46.737301"
+       height="46.0728"
+       filterUnits="userSpaceOnUse"
+       color-interpolation-filters="sRGB">
+      <feFlood
+         flood-opacity="0"
+         result="BackgroundImageFix"
+         id="feFlood5" />
+      <feBlend
+         mode="normal"
+         in="SourceGraphic"
+         in2="BackgroundImageFix"
+         result="shape"
+         id="feBlend6" />
+      <feColorMatrix
+         in="SourceAlpha"
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
+         result="hardAlpha"
+         id="feColorMatrix6" />
+      <feOffset
+         dx="-1"
+         dy="-1"
+         id="feOffset6" />
+      <feGaussianBlur
+         stdDeviation="1"
+         id="feGaussianBlur6" />
+      <feComposite
+         in2="hardAlpha"
+         operator="arithmetic"
+         k2="-1"
+         k3="1"
+         id="feComposite6"
+         k1="0"
+         k4="0" />
+      <feColorMatrix
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0"
+         id="feColorMatrix7" />
+      <feBlend
+         mode="normal"
+         in2="shape"
+         result="effect1_innerShadow_8602_13905"
+         id="feBlend7" />
+    </filter>
+    <radialGradient
+       id="paint0_radial_8602_13905"
+       cx="0"
+       cy="0"
+       r="1"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(15.90363,43.662615,-43.662615,15.90363,30.4578,26.4096)">
+      <stop
+         stop-color="#FAE2C0"
+         id="stop7" />
+      <stop
+         offset="1"
+         stop-color="#5D3E1F"
+         id="stop8" />
+    </radialGradient>
+    <radialGradient
+       id="paint1_angular_8602_13905"
+       cx="0"
+       cy="0"
+       r="1"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-63.325498,23.999469,-23.999469,-63.325498,69.2048,24.6747)">
+      <stop
+         offset="0.000197635"
+         stop-color="#BFA27A"
+         id="stop9" />
+      <stop
+         offset="1"
+         stop-color="#5D3E1F"
+         id="stop10" />
+    </radialGradient>
+  </defs>
+</svg>
diff --git a/assets/badges/badge_empty.svg b/assets/badges/badge_empty.svg
new file mode 100644
index 00000000..051414ab
--- /dev/null
+++ b/assets/badges/badge_empty.svg
@@ -0,0 +1,3 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path opacity="0.3" d="M23.1697 1.23711C23.5659 0.646827 24.4341 0.646828 24.8303 1.23711L29.257 7.83252C29.4994 8.19363 29.9474 8.35669 30.3652 8.23585L37.9956 6.02889C38.6786 5.83137 39.3437 6.38949 39.2678 7.09633L38.4194 14.9941C38.3729 15.4266 38.6113 15.8394 39.009 16.0154L46.2729 19.2296C46.923 19.5172 47.0738 20.3723 46.5613 20.865L40.8347 26.3697C40.5212 26.6711 40.4384 27.1406 40.63 27.531L44.1284 34.6624C44.4415 35.3006 44.0074 36.0526 43.2981 36.1005L35.373 36.6364C34.939 36.6658 34.5738 36.9722 34.4696 37.3945L32.5656 45.1061C32.3952 45.7963 31.5793 46.0933 31.0051 45.6741L24.5896 40.9905C24.2384 40.734 23.7616 40.734 23.4104 40.9905L16.9949 45.6741C16.4207 46.0933 15.6048 45.7963 15.4344 45.1061L13.5304 37.3945C13.4262 36.9722 13.061 36.6658 12.627 36.6364L4.70192 36.1005C3.99263 36.0526 3.55849 35.3006 3.8716 34.6624L7.37005 27.531C7.5616 27.1406 7.47881 26.6711 7.16527 26.3697L1.43874 20.865C0.926228 20.3723 1.077 19.5172 1.72711 19.2296L8.991 16.0154C9.38873 15.8394 9.6271 15.4266 9.58064 14.9941L8.73223 7.09633C8.6563 6.38949 9.32144 5.83137 10.0044 6.02889L17.6348 8.23585C18.0526 8.35669 18.5006 8.19363 18.743 7.83251L23.1697 1.23711Z" stroke="#005437" stroke-width="1" stroke-linecap="round" stroke-dasharray="2 3"/>
+</svg>
diff --git a/assets/badges/badge_gold.svg b/assets/badges/badge_gold.svg
new file mode 100644
index 00000000..2dc66e9b
--- /dev/null
+++ b/assets/badges/badge_gold.svg
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="46.737301"
+   height="46.0728"
+   viewBox="0 0 46.737301 46.0728"
+   fill="none"
+   version="1.1"
+   id="svg10"
+   sodipodi:docname="badge_gold.svg"
+   inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview10"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="8.1956522"
+     inkscape:cx="27.880637"
+     inkscape:cy="27.209549"
+     inkscape:window-width="1920"
+     inkscape:window-height="992"
+     inkscape:window-x="2392"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg10" />
+  <g
+     filter="url(#filter1_i_8602_13910)"
+     id="g2"
+     transform="translate(-16.1313,-18.794399)">
+    <path
+       d="m 39.1697,20.2371 c 0.3962,-0.5903 1.2644,-0.5903 1.6606,0 l 4.4267,6.5954 c 0.2424,0.3611 0.6904,0.5242 1.1082,0.4034 l 7.6304,-2.207 c 0.683,-0.1975 1.3481,0.3606 1.2722,1.0674 l -0.8484,7.8978 c -0.0465,0.4325 0.1919,0.8453 0.5896,1.0213 l 7.2639,3.2142 c 0.6501,0.2876 0.8009,1.1427 0.2884,1.6354 l -5.7266,5.5047 c -0.3135,0.3014 -0.3963,0.7709 -0.2047,1.1613 l 3.4984,7.1314 c 0.3131,0.6382 -0.121,1.3902 -0.8303,1.4381 l -7.9251,0.5359 c -0.434,0.0294 -0.7992,0.3358 -0.9034,0.7581 l -1.904,7.7116 c -0.1704,0.6902 -0.9863,0.9872 -1.5605,0.568 l -6.4155,-4.6836 c -0.3512,-0.2565 -0.828,-0.2565 -1.1792,0 l -6.4155,4.6836 c -0.5742,0.4192 -1.3901,0.1222 -1.5605,-0.568 l -1.904,-7.7116 C 29.4262,55.9722 29.061,55.6658 28.627,55.6364 L 20.7019,55.1005 C 19.9926,55.0526 19.5585,54.3006 19.8716,53.6624 L 23.37,46.531 c 0.1916,-0.3904 0.1088,-0.8599 -0.2047,-1.1613 L 17.4387,39.865 c -0.5125,-0.4927 -0.3617,-1.3478 0.2884,-1.6354 l 7.2639,-3.2142 c 0.3977,-0.176 0.6361,-0.5888 0.5896,-1.0213 l -0.8484,-7.8978 c -0.0759,-0.7068 0.5892,-1.2649 1.2722,-1.0674 l 7.6304,2.207 c 0.4178,0.1208 0.8658,-0.0423 1.1082,-0.4034 z"
+       fill="url(#paint0_radial_8602_13910)"
+       id="path1"
+       style="fill:url(#paint0_radial_8602_13910)" />
+    <path
+       d="m 39.1697,20.2371 c 0.3962,-0.5903 1.2644,-0.5903 1.6606,0 l 4.4267,6.5954 c 0.2424,0.3611 0.6904,0.5242 1.1082,0.4034 l 7.6304,-2.207 c 0.683,-0.1975 1.3481,0.3606 1.2722,1.0674 l -0.8484,7.8978 c -0.0465,0.4325 0.1919,0.8453 0.5896,1.0213 l 7.2639,3.2142 c 0.6501,0.2876 0.8009,1.1427 0.2884,1.6354 l -5.7266,5.5047 c -0.3135,0.3014 -0.3963,0.7709 -0.2047,1.1613 l 3.4984,7.1314 c 0.3131,0.6382 -0.121,1.3902 -0.8303,1.4381 l -7.9251,0.5359 c -0.434,0.0294 -0.7992,0.3358 -0.9034,0.7581 l -1.904,7.7116 c -0.1704,0.6902 -0.9863,0.9872 -1.5605,0.568 l -6.4155,-4.6836 c -0.3512,-0.2565 -0.828,-0.2565 -1.1792,0 l -6.4155,4.6836 c -0.5742,0.4192 -1.3901,0.1222 -1.5605,-0.568 l -1.904,-7.7116 C 29.4262,55.9722 29.061,55.6658 28.627,55.6364 L 20.7019,55.1005 C 19.9926,55.0526 19.5585,54.3006 19.8716,53.6624 L 23.37,46.531 c 0.1916,-0.3904 0.1088,-0.8599 -0.2047,-1.1613 L 17.4387,39.865 c -0.5125,-0.4927 -0.3617,-1.3478 0.2884,-1.6354 l 7.2639,-3.2142 c 0.3977,-0.176 0.6361,-0.5888 0.5896,-1.0213 l -0.8484,-7.8978 c -0.0759,-0.7068 0.5892,-1.2649 1.2722,-1.0674 l 7.6304,2.207 c 0.4178,0.1208 0.8658,-0.0423 1.1082,-0.4034 z"
+       fill="url(#paint1_angular_8602_13910)"
+       fill-opacity="0.1"
+       id="path2"
+       style="fill:url(#paint1_angular_8602_13910)" />
+  </g>
+  <defs
+     id="defs10">
+    <filter
+       id="filter0_d_8602_13910"
+       x="-5"
+       y="0"
+       width="94"
+       height="94"
+       filterUnits="userSpaceOnUse"
+       color-interpolation-filters="sRGB">
+      <feFlood
+         flood-opacity="0"
+         result="BackgroundImageFix"
+         id="feFlood3" />
+      <feColorMatrix
+         in="SourceAlpha"
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
+         result="hardAlpha"
+         id="feColorMatrix3" />
+      <feMorphology
+         radius="7"
+         operator="dilate"
+         in="SourceAlpha"
+         result="effect1_dropShadow_8602_13910"
+         id="feMorphology3" />
+      <feOffset
+         dx="2"
+         dy="4"
+         id="feOffset3" />
+      <feGaussianBlur
+         stdDeviation="8"
+         id="feGaussianBlur3" />
+      <feColorMatrix
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
+         id="feColorMatrix4" />
+      <feBlend
+         mode="normal"
+         in2="BackgroundImageFix"
+         result="effect1_dropShadow_8602_13910"
+         id="feBlend4" />
+      <feBlend
+         mode="normal"
+         in="SourceGraphic"
+         in2="effect1_dropShadow_8602_13910"
+         result="shape"
+         id="feBlend5" />
+    </filter>
+    <filter
+       id="filter1_i_8602_13910"
+       x="16.1313"
+       y="18.794399"
+       width="46.737301"
+       height="46.0728"
+       filterUnits="userSpaceOnUse"
+       color-interpolation-filters="sRGB">
+      <feFlood
+         flood-opacity="0"
+         result="BackgroundImageFix"
+         id="feFlood5" />
+      <feBlend
+         mode="normal"
+         in="SourceGraphic"
+         in2="BackgroundImageFix"
+         result="shape"
+         id="feBlend6" />
+      <feColorMatrix
+         in="SourceAlpha"
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
+         result="hardAlpha"
+         id="feColorMatrix6" />
+      <feOffset
+         dx="-1"
+         dy="-1"
+         id="feOffset6" />
+      <feGaussianBlur
+         stdDeviation="1"
+         id="feGaussianBlur6" />
+      <feComposite
+         in2="hardAlpha"
+         operator="arithmetic"
+         k2="-1"
+         k3="1"
+         id="feComposite6"
+         k1="0"
+         k4="0" />
+      <feColorMatrix
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0"
+         id="feColorMatrix7" />
+      <feBlend
+         mode="normal"
+         in2="shape"
+         result="effect1_innerShadow_8602_13910"
+         id="feBlend7" />
+    </filter>
+    <radialGradient
+       id="paint0_radial_8602_13910"
+       cx="0"
+       cy="0"
+       r="1"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(15.90363,43.662615,-43.662615,15.90363,30.4578,29.4096)">
+      <stop
+         stop-color="#FFE793"
+         id="stop7" />
+      <stop
+         offset="1"
+         stop-color="#7D5520"
+         id="stop8" />
+    </radialGradient>
+    <radialGradient
+       id="paint1_angular_8602_13910"
+       cx="0"
+       cy="0"
+       r="1"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-71.421428,32.386173,-32.386173,-71.421428,64,25.0723)">
+      <stop
+         stop-color="#F8D763"
+         id="stop9" />
+      <stop
+         offset="1"
+         stop-color="#7D5520"
+         id="stop10" />
+    </radialGradient>
+  </defs>
+</svg>
diff --git a/assets/badges/badge_platinum.svg b/assets/badges/badge_platinum.svg
new file mode 100644
index 00000000..12131cbc
--- /dev/null
+++ b/assets/badges/badge_platinum.svg
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="46.737301"
+   height="46.0728"
+   viewBox="0 0 46.737301 46.0728"
+   fill="none"
+   version="1.1"
+   id="svg10"
+   sodipodi:docname="badge_platinum.svg"
+   inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview10"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="8.1956522"
+     inkscape:cx="27.880637"
+     inkscape:cy="27.209549"
+     inkscape:window-width="1920"
+     inkscape:window-height="992"
+     inkscape:window-x="2392"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg10" />
+  <g
+     filter="url(#filter1_i_8602_13913)"
+     id="g2"
+     transform="translate(-16.1313,-18.794399)">
+    <path
+       d="m 39.1697,20.2371 c 0.3962,-0.5903 1.2644,-0.5903 1.6606,0 l 4.4267,6.5954 c 0.2424,0.3611 0.6904,0.5242 1.1082,0.4034 l 7.6304,-2.207 c 0.683,-0.1975 1.3481,0.3606 1.2722,1.0674 l -0.8484,7.8978 c -0.0465,0.4325 0.1919,0.8453 0.5896,1.0213 l 7.2639,3.2142 c 0.6501,0.2876 0.8009,1.1427 0.2884,1.6354 l -5.7266,5.5047 c -0.3135,0.3014 -0.3963,0.7709 -0.2047,1.1613 l 3.4984,7.1314 c 0.3131,0.6382 -0.121,1.3902 -0.8303,1.4381 l -7.9251,0.5359 c -0.434,0.0294 -0.7992,0.3358 -0.9034,0.7581 l -1.904,7.7116 c -0.1704,0.6902 -0.9863,0.9872 -1.5605,0.568 l -6.4155,-4.6836 c -0.3512,-0.2565 -0.828,-0.2565 -1.1792,0 l -6.4155,4.6836 c -0.5742,0.4192 -1.3901,0.1222 -1.5605,-0.568 l -1.904,-7.7116 C 29.4262,55.9722 29.061,55.6658 28.627,55.6364 L 20.7019,55.1005 C 19.9926,55.0526 19.5585,54.3006 19.8716,53.6624 L 23.37,46.531 c 0.1916,-0.3904 0.1088,-0.8599 -0.2047,-1.1613 L 17.4387,39.865 c -0.5125,-0.4927 -0.3617,-1.3478 0.2884,-1.6354 l 7.2639,-3.2142 c 0.3977,-0.176 0.6361,-0.5888 0.5896,-1.0213 l -0.8484,-7.8978 c -0.0759,-0.7068 0.5892,-1.2649 1.2722,-1.0674 l 7.6304,2.207 c 0.4178,0.1208 0.8658,-0.0423 1.1082,-0.4034 z"
+       fill="url(#paint0_radial_8602_13913)"
+       id="path1"
+       style="fill:url(#paint0_radial_8602_13913)" />
+    <path
+       d="m 39.1697,20.2371 c 0.3962,-0.5903 1.2644,-0.5903 1.6606,0 l 4.4267,6.5954 c 0.2424,0.3611 0.6904,0.5242 1.1082,0.4034 l 7.6304,-2.207 c 0.683,-0.1975 1.3481,0.3606 1.2722,1.0674 l -0.8484,7.8978 c -0.0465,0.4325 0.1919,0.8453 0.5896,1.0213 l 7.2639,3.2142 c 0.6501,0.2876 0.8009,1.1427 0.2884,1.6354 l -5.7266,5.5047 c -0.3135,0.3014 -0.3963,0.7709 -0.2047,1.1613 l 3.4984,7.1314 c 0.3131,0.6382 -0.121,1.3902 -0.8303,1.4381 l -7.9251,0.5359 c -0.434,0.0294 -0.7992,0.3358 -0.9034,0.7581 l -1.904,7.7116 c -0.1704,0.6902 -0.9863,0.9872 -1.5605,0.568 l -6.4155,-4.6836 c -0.3512,-0.2565 -0.828,-0.2565 -1.1792,0 l -6.4155,4.6836 c -0.5742,0.4192 -1.3901,0.1222 -1.5605,-0.568 l -1.904,-7.7116 C 29.4262,55.9722 29.061,55.6658 28.627,55.6364 L 20.7019,55.1005 C 19.9926,55.0526 19.5585,54.3006 19.8716,53.6624 L 23.37,46.531 c 0.1916,-0.3904 0.1088,-0.8599 -0.2047,-1.1613 L 17.4387,39.865 c -0.5125,-0.4927 -0.3617,-1.3478 0.2884,-1.6354 l 7.2639,-3.2142 c 0.3977,-0.176 0.6361,-0.5888 0.5896,-1.0213 l -0.8484,-7.8978 c -0.0759,-0.7068 0.5892,-1.2649 1.2722,-1.0674 l 7.6304,2.207 c 0.4178,0.1208 0.8658,-0.0423 1.1082,-0.4034 z"
+       fill="url(#paint1_angular_8602_13913)"
+       fill-opacity="0.1"
+       id="path2"
+       style="fill:url(#paint1_angular_8602_13913)" />
+  </g>
+  <defs
+     id="defs10">
+    <filter
+       id="filter0_d_8602_13913"
+       x="-5"
+       y="0"
+       width="94"
+       height="94"
+       filterUnits="userSpaceOnUse"
+       color-interpolation-filters="sRGB">
+      <feFlood
+         flood-opacity="0"
+         result="BackgroundImageFix"
+         id="feFlood3" />
+      <feColorMatrix
+         in="SourceAlpha"
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
+         result="hardAlpha"
+         id="feColorMatrix3" />
+      <feMorphology
+         radius="7"
+         operator="dilate"
+         in="SourceAlpha"
+         result="effect1_dropShadow_8602_13913"
+         id="feMorphology3" />
+      <feOffset
+         dx="2"
+         dy="4"
+         id="feOffset3" />
+      <feGaussianBlur
+         stdDeviation="8"
+         id="feGaussianBlur3" />
+      <feColorMatrix
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
+         id="feColorMatrix4" />
+      <feBlend
+         mode="normal"
+         in2="BackgroundImageFix"
+         result="effect1_dropShadow_8602_13913"
+         id="feBlend4" />
+      <feBlend
+         mode="normal"
+         in="SourceGraphic"
+         in2="effect1_dropShadow_8602_13913"
+         result="shape"
+         id="feBlend5" />
+    </filter>
+    <filter
+       id="filter1_i_8602_13913"
+       x="16.1313"
+       y="18.794399"
+       width="46.737301"
+       height="46.0728"
+       filterUnits="userSpaceOnUse"
+       color-interpolation-filters="sRGB">
+      <feFlood
+         flood-opacity="0"
+         result="BackgroundImageFix"
+         id="feFlood5" />
+      <feBlend
+         mode="normal"
+         in="SourceGraphic"
+         in2="BackgroundImageFix"
+         result="shape"
+         id="feBlend6" />
+      <feColorMatrix
+         in="SourceAlpha"
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
+         result="hardAlpha"
+         id="feColorMatrix6" />
+      <feOffset
+         dx="-1"
+         dy="-1"
+         id="feOffset6" />
+      <feGaussianBlur
+         stdDeviation="1"
+         id="feGaussianBlur6" />
+      <feComposite
+         in2="hardAlpha"
+         operator="arithmetic"
+         k2="-1"
+         k3="1"
+         id="feComposite6"
+         k1="0"
+         k4="0" />
+      <feColorMatrix
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0"
+         id="feColorMatrix7" />
+      <feBlend
+         mode="normal"
+         in2="shape"
+         result="effect1_innerShadow_8602_13913"
+         id="feBlend7" />
+    </filter>
+    <radialGradient
+       id="paint0_radial_8602_13913"
+       cx="0"
+       cy="0"
+       r="1"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(15.90363,43.662615,-43.662615,15.90363,30.4578,29.4096)">
+      <stop
+         stop-color="white"
+         id="stop7" />
+      <stop
+         offset="1"
+         stop-color="#878789"
+         id="stop8" />
+    </radialGradient>
+    <radialGradient
+       id="paint1_angular_8602_13913"
+       cx="0"
+       cy="0"
+       r="1"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-57.831144,23.711206,-23.711206,-57.831144,69.2048,26.5181)">
+      <stop
+         stop-color="#ECECEE"
+         id="stop9" />
+      <stop
+         offset="1"
+         stop-color="#878789"
+         id="stop10" />
+    </radialGradient>
+  </defs>
+</svg>
diff --git a/assets/badges/badge_silver.svg b/assets/badges/badge_silver.svg
new file mode 100644
index 00000000..21673012
--- /dev/null
+++ b/assets/badges/badge_silver.svg
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="46.737301"
+   height="46.0728"
+   viewBox="0 0 46.737301 46.0728"
+   fill="none"
+   version="1.1"
+   id="svg10"
+   sodipodi:docname="badge_silver.svg"
+   inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview10"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="8.1956522"
+     inkscape:cx="27.880637"
+     inkscape:cy="27.209549"
+     inkscape:window-width="1920"
+     inkscape:window-height="992"
+     inkscape:window-x="2392"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg10" />
+  <g
+     filter="url(#filter1_i_8602_13907)"
+     id="g2"
+     transform="translate(-16.1313,-18.794399)">
+    <path
+       d="m 39.1697,20.2371 c 0.3962,-0.5903 1.2644,-0.5903 1.6606,0 l 4.4267,6.5954 c 0.2424,0.3611 0.6904,0.5242 1.1082,0.4034 l 7.6304,-2.207 c 0.683,-0.1975 1.3481,0.3606 1.2722,1.0674 l -0.8484,7.8978 c -0.0465,0.4325 0.1919,0.8453 0.5896,1.0213 l 7.2639,3.2142 c 0.6501,0.2876 0.8009,1.1427 0.2884,1.6354 l -5.7266,5.5047 c -0.3135,0.3014 -0.3963,0.7709 -0.2047,1.1613 l 3.4984,7.1314 c 0.3131,0.6382 -0.121,1.3902 -0.8303,1.4381 l -7.9251,0.5359 c -0.434,0.0294 -0.7992,0.3358 -0.9034,0.7581 l -1.904,7.7116 c -0.1704,0.6902 -0.9863,0.9872 -1.5605,0.568 l -6.4155,-4.6836 c -0.3512,-0.2565 -0.828,-0.2565 -1.1792,0 l -6.4155,4.6836 c -0.5742,0.4192 -1.3901,0.1222 -1.5605,-0.568 l -1.904,-7.7116 C 29.4262,55.9722 29.061,55.6658 28.627,55.6364 L 20.7019,55.1005 C 19.9926,55.0526 19.5585,54.3006 19.8716,53.6624 L 23.37,46.531 c 0.1916,-0.3904 0.1088,-0.8599 -0.2047,-1.1613 L 17.4387,39.865 c -0.5125,-0.4927 -0.3617,-1.3478 0.2884,-1.6354 l 7.2639,-3.2142 c 0.3977,-0.176 0.6361,-0.5888 0.5896,-1.0213 l -0.8484,-7.8978 c -0.0759,-0.7068 0.5892,-1.2649 1.2722,-1.0674 l 7.6304,2.207 c 0.4178,0.1208 0.8658,-0.0423 1.1082,-0.4034 z"
+       fill="url(#paint0_radial_8602_13907)"
+       id="path1"
+       style="fill:url(#paint0_radial_8602_13907)" />
+    <path
+       d="m 39.1697,20.2371 c 0.3962,-0.5903 1.2644,-0.5903 1.6606,0 l 4.4267,6.5954 c 0.2424,0.3611 0.6904,0.5242 1.1082,0.4034 l 7.6304,-2.207 c 0.683,-0.1975 1.3481,0.3606 1.2722,1.0674 l -0.8484,7.8978 c -0.0465,0.4325 0.1919,0.8453 0.5896,1.0213 l 7.2639,3.2142 c 0.6501,0.2876 0.8009,1.1427 0.2884,1.6354 l -5.7266,5.5047 c -0.3135,0.3014 -0.3963,0.7709 -0.2047,1.1613 l 3.4984,7.1314 c 0.3131,0.6382 -0.121,1.3902 -0.8303,1.4381 l -7.9251,0.5359 c -0.434,0.0294 -0.7992,0.3358 -0.9034,0.7581 l -1.904,7.7116 c -0.1704,0.6902 -0.9863,0.9872 -1.5605,0.568 l -6.4155,-4.6836 c -0.3512,-0.2565 -0.828,-0.2565 -1.1792,0 l -6.4155,4.6836 c -0.5742,0.4192 -1.3901,0.1222 -1.5605,-0.568 l -1.904,-7.7116 C 29.4262,55.9722 29.061,55.6658 28.627,55.6364 L 20.7019,55.1005 C 19.9926,55.0526 19.5585,54.3006 19.8716,53.6624 L 23.37,46.531 c 0.1916,-0.3904 0.1088,-0.8599 -0.2047,-1.1613 L 17.4387,39.865 c -0.5125,-0.4927 -0.3617,-1.3478 0.2884,-1.6354 l 7.2639,-3.2142 c 0.3977,-0.176 0.6361,-0.5888 0.5896,-1.0213 l -0.8484,-7.8978 c -0.0759,-0.7068 0.5892,-1.2649 1.2722,-1.0674 l 7.6304,2.207 c 0.4178,0.1208 0.8658,-0.0423 1.1082,-0.4034 z"
+       fill="url(#paint1_angular_8602_13907)"
+       fill-opacity="0.1"
+       id="path2"
+       style="fill:url(#paint1_angular_8602_13907)" />
+  </g>
+  <defs
+     id="defs10">
+    <filter
+       id="filter0_d_8602_13907"
+       x="-5"
+       y="0"
+       width="94"
+       height="94"
+       filterUnits="userSpaceOnUse"
+       color-interpolation-filters="sRGB">
+      <feFlood
+         flood-opacity="0"
+         result="BackgroundImageFix"
+         id="feFlood3" />
+      <feColorMatrix
+         in="SourceAlpha"
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
+         result="hardAlpha"
+         id="feColorMatrix3" />
+      <feMorphology
+         radius="7"
+         operator="dilate"
+         in="SourceAlpha"
+         result="effect1_dropShadow_8602_13907"
+         id="feMorphology3" />
+      <feOffset
+         dx="2"
+         dy="4"
+         id="feOffset3" />
+      <feGaussianBlur
+         stdDeviation="8"
+         id="feGaussianBlur3" />
+      <feColorMatrix
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
+         id="feColorMatrix4" />
+      <feBlend
+         mode="normal"
+         in2="BackgroundImageFix"
+         result="effect1_dropShadow_8602_13907"
+         id="feBlend4" />
+      <feBlend
+         mode="normal"
+         in="SourceGraphic"
+         in2="effect1_dropShadow_8602_13907"
+         result="shape"
+         id="feBlend5" />
+    </filter>
+    <filter
+       id="filter1_i_8602_13907"
+       x="16.1313"
+       y="18.794399"
+       width="46.737301"
+       height="46.0728"
+       filterUnits="userSpaceOnUse"
+       color-interpolation-filters="sRGB">
+      <feFlood
+         flood-opacity="0"
+         result="BackgroundImageFix"
+         id="feFlood5" />
+      <feBlend
+         mode="normal"
+         in="SourceGraphic"
+         in2="BackgroundImageFix"
+         result="shape"
+         id="feBlend6" />
+      <feColorMatrix
+         in="SourceAlpha"
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
+         result="hardAlpha"
+         id="feColorMatrix6" />
+      <feOffset
+         dx="-1"
+         dy="-1"
+         id="feOffset6" />
+      <feGaussianBlur
+         stdDeviation="1"
+         id="feGaussianBlur6" />
+      <feComposite
+         in2="hardAlpha"
+         operator="arithmetic"
+         k2="-1"
+         k3="1"
+         id="feComposite6"
+         k1="0"
+         k4="0" />
+      <feColorMatrix
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0"
+         id="feColorMatrix7" />
+      <feBlend
+         mode="normal"
+         in2="shape"
+         result="effect1_innerShadow_8602_13907"
+         id="feBlend7" />
+    </filter>
+    <radialGradient
+       id="paint0_radial_8602_13907"
+       cx="0"
+       cy="0"
+       r="1"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(15.90363,43.662615,-43.662615,15.90363,30.4578,29.4096)">
+      <stop
+         stop-color="#DCE6F0"
+         id="stop7" />
+      <stop
+         offset="1"
+         stop-color="#515153"
+         id="stop8" />
+    </radialGradient>
+    <radialGradient
+       id="paint1_angular_8602_13907"
+       cx="0"
+       cy="0"
+       r="1"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-61.879424,26.31343,-26.31343,-61.879424,67.1807,24.7831)">
+      <stop
+         stop-color="#C1C3C5"
+         id="stop9" />
+      <stop
+         offset="1"
+         stop-color="#515153"
+         id="stop10" />
+    </radialGradient>
+  </defs>
+</svg>
diff --git a/lib/app/services/gruene_api_campaigns_statistics_service.dart b/lib/app/services/gruene_api_campaigns_statistics_service.dart
new file mode 100644
index 00000000..21ff5de0
--- /dev/null
+++ b/lib/app/services/gruene_api_campaigns_statistics_service.dart
@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+import 'package:get_it/get_it.dart';
+import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart';
+
+class GrueneApiCampaignsStatisticsService {
+  late GrueneApi grueneApi;
+
+  GrueneApiCampaignsStatisticsService() {
+    grueneApi = GetIt.I<GrueneApi>();
+  }
+
+  Future<CampaignStatistics> getStatistics() async {
+    try {
+      var statResult = await grueneApi.v1CampaignsStatisticsGet();
+      return statResult.body!.asCampaignStatistics();
+    } catch (e, s) {
+      debugPrint(e.toString());
+      debugPrint(s.toString());
+      rethrow;
+    }
+  }
+}
+
+extension StatisticsParser on Statistics {
+  CampaignStatistics asCampaignStatistics() {
+    return CampaignStatistics(
+      flyerStats: flyer.asStatisticsSet(),
+      houseStats: house.asStatisticsSet(),
+      posterStats: poster.asStatisticsSet(),
+    );
+  }
+}
+
+extension PoiStatisticsParser on PoiStatistics {
+  CampaignStatisticsSet asStatisticsSet() {
+    return CampaignStatisticsSet(
+      own: own,
+      division: division,
+      state: state,
+      germany: germany,
+    );
+  }
+}
+
+class CampaignStatisticsSet {
+  final double own, division, state, germany;
+
+  const CampaignStatisticsSet({required this.own, required this.division, required this.state, required this.germany});
+}
+
+class CampaignStatistics {
+  final CampaignStatisticsSet flyerStats, houseStats, posterStats;
+
+  const CampaignStatistics({
+    required this.flyerStats,
+    required this.houseStats,
+    required this.posterStats,
+  });
+}
diff --git a/lib/app/theme/theme.dart b/lib/app/theme/theme.dart
index 2909987c..9beff5e7 100644
--- a/lib/app/theme/theme.dart
+++ b/lib/app/theme/theme.dart
@@ -8,7 +8,7 @@ class ThemeColors {
   // Secondary Light Green (#008939)
   static const Color secondary = Color(0xFF008939);
 
-  // Secondary Light Green (#008939)
+  // Tertiary Light Green (#46962B)
   static const Color tertiary = Color(0xFF46962B);
 
   // White (#FFFFFF)
diff --git a/lib/features/campaigns/screens/statistics_screen.dart b/lib/features/campaigns/screens/statistics_screen.dart
index 55e07184..b7f7778e 100644
--- a/lib/features/campaigns/screens/statistics_screen.dart
+++ b/lib/features/campaigns/screens/statistics_screen.dart
@@ -1,7 +1,9 @@
-import 'dart:math';
-
 import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:get_it/get_it.dart';
+import 'package:gruene_app/app/services/gruene_api_campaigns_statistics_service.dart';
 import 'package:gruene_app/app/theme/theme.dart';
+import 'package:gruene_app/features/campaigns/helper/campaign_constants.dart';
 import 'package:gruene_app/i18n/translations.g.dart';
 import 'package:intl/intl.dart';
 
@@ -12,34 +14,52 @@ class StatisticsScreen extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = Theme.of(context);
 
+    return FutureBuilder(
+      future: _loadStatistics(),
+      builder: (context, snapshot) {
+        if (!snapshot.hasData || snapshot.hasError) {
+          return Image.asset(CampaignConstants.dummyImageAssetName);
+        }
+        return _buildStatScreen(snapshot.data!, theme, context);
+      },
+    );
+  }
+
+  SingleChildScrollView _buildStatScreen(CampaignStatistics statistics, ThemeData theme, BuildContext context) {
     return SingleChildScrollView(
       child: Container(
         padding: EdgeInsets.all(16),
         color: theme.colorScheme.surfaceDim,
         child: Column(
           children: [
-            _getBadgeBox(context, theme),
+            _getBadgeBox(statistics, context, theme),
             SizedBox(height: 12),
             _getCategoryBox(
+              stats: statistics.houseStats,
               theme: theme,
               title: t.campaigns.statistic.recorded_doors,
             ),
             SizedBox(height: 12),
             _getCategoryBox(
+              stats: statistics.posterStats,
               theme: theme,
               title: t.campaigns.statistic.recorded_posters,
               subTitle: t.campaigns.statistic.including_damaged_or_taken_down,
             ),
             SizedBox(height: 12),
             _getCategoryBox(
+              stats: statistics.flyerStats,
               theme: theme,
               title: t.campaigns.statistic.recorded_flyer,
             ),
-            Align(
-              alignment: Alignment.centerLeft,
-              child: Text(
-                'Stand: ${DateTime.now().toString()} (${t.campaigns.statistic.update_info})',
-                style: theme.textTheme.labelMedium!.apply(color: ThemeColors.textDisabled),
+            Container(
+              padding: EdgeInsets.all(16),
+              child: Align(
+                alignment: Alignment.centerLeft,
+                child: Text(
+                  'Stand: ${DateTime.now().toString()} (${t.campaigns.statistic.update_info})',
+                  style: theme.textTheme.labelMedium!.apply(color: ThemeColors.textDisabled),
+                ),
               ),
             ),
           ],
@@ -48,13 +68,13 @@ class StatisticsScreen extends StatelessWidget {
     );
   }
 
-  Widget _getBadgeBox(BuildContext context, ThemeData theme) {
+  Widget _getBadgeBox(CampaignStatistics statistics, BuildContext context, ThemeData theme) {
     var mediaQuery = MediaQuery.of(context);
     return Container(
       padding: EdgeInsets.all(16),
       width: mediaQuery.size.width,
       decoration: BoxDecoration(
-        color: ThemeColors.primary,
+        color: ThemeColors.background,
         borderRadius: BorderRadius.circular(19),
         boxShadow: [
           BoxShadow(color: ThemeColors.textDark.withAlpha(10), offset: Offset(2, 4)),
@@ -66,20 +86,124 @@ class StatisticsScreen extends StatelessWidget {
             alignment: Alignment.centerLeft,
             child: Text(
               t.campaigns.statistic.my_badges,
-              style: theme.textTheme.titleMedium!.copyWith(color: theme.colorScheme.surface),
+              style: theme.textTheme.titleMedium,
+            ),
+          ),
+          Align(
+            alignment: Alignment.centerLeft,
+            child: Text(
+              t.campaigns.statistic.my_badges_campaign_subtitle,
+              style: theme.textTheme.labelSmall,
             ),
           ),
-          ..._getBadges(),
+          ..._getBadges(statistics, theme),
         ],
       ),
     );
   }
 
-  List<Widget> _getBadges() {
-    return <Widget>[];
+  List<Widget> _getBadges(CampaignStatistics statistics, ThemeData theme) {
+    return [
+      _getBadgeRow(t.campaigns.statistic.recorded_doors, statistics.houseStats.own.toInt(), theme),
+      _getBadgeRow(t.campaigns.statistic.recorded_posters, statistics.posterStats.own.toInt(), theme),
+      _getBadgeRow(t.campaigns.statistic.recorded_flyer, statistics.flyerStats.own.toInt(), theme),
+    ];
   }
 
-  Widget _getCategoryBox({required String title, String? subTitle, required ThemeData theme}) {
+  Widget _getBadgeRow(String title, int ownCounter, ThemeData theme) {
+    // var rng = Random();
+    return Container(
+      decoration: BoxDecoration(
+        border: Border(
+          bottom: BorderSide(color: ThemeColors.textLight),
+        ),
+      ),
+      padding: EdgeInsets.all(4),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Text(title, style: theme.textTheme.labelLarge!.copyWith(color: ThemeColors.textDark)),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            children: [
+              ..._getBadgeIcons(ownCounter, theme),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+
+  List<Widget> _getBadgeIcons(int value, ThemeData theme) {
+    var thresholds = [50, 100, 250, 500];
+    var badges = ['bronze', 'silver', 'gold', 'platinum'];
+    var widgets = <Widget>[];
+    var iconSize = 50.0;
+    for (var i = 0; i < thresholds.length; i++) {
+      var currentThreshold = thresholds[i];
+      if (currentThreshold < value) {
+        widgets.add(
+          SizedBox(
+            height: iconSize,
+            child: Stack(
+              children: [
+                SvgPicture.asset(
+                  'assets/badges/badge_${badges[i]}.svg',
+                  fit: BoxFit.fill,
+                  height: iconSize,
+                  width: iconSize,
+                ),
+                Positioned.fill(
+                  child: Align(
+                    alignment: Alignment.center,
+                    child: Text(
+                      currentThreshold.toString(),
+                      style: theme.textTheme.labelMedium!.apply(fontWeightDelta: 3),
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        );
+      } else {
+        widgets.add(
+          SizedBox(
+            height: iconSize,
+            child: Stack(
+              children: [
+                SvgPicture.asset(
+                  'assets/badges/badge_empty.svg',
+                  fit: BoxFit.fill,
+                  height: iconSize,
+                  width: iconSize,
+                ),
+                Positioned.fill(
+                  child: Align(
+                    alignment: Alignment.center,
+                    child: Text(
+                      currentThreshold.toString(),
+                      style: theme.textTheme.labelMedium!
+                          .apply(fontWeightDelta: 3, color: theme.colorScheme.primary.withOpacity(0.3)),
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        );
+      }
+      widgets.add(SizedBox(width: 6));
+    }
+    return widgets;
+  }
+
+  Widget _getCategoryBox({
+    required String title,
+    String? subTitle,
+    required ThemeData theme,
+    required CampaignStatisticsSet stats,
+  }) {
     var categoryDecoration = BoxDecoration(
       color: ThemeColors.background,
       borderRadius: BorderRadius.circular(19),
@@ -87,7 +211,6 @@ class StatisticsScreen extends StatelessWidget {
         BoxShadow(color: ThemeColors.textDark.withAlpha(10), offset: Offset(2, 4)),
       ],
     );
-    var rng = Random();
     return Container(
       padding: EdgeInsets.all(16),
       decoration: categoryDecoration,
@@ -111,10 +234,10 @@ class StatisticsScreen extends StatelessWidget {
                   ],
                 )
               : SizedBox(),
-          _getDataRow(t.campaigns.statistic.by_me, rng.nextInt(200), theme),
-          _getDataRow(t.campaigns.statistic.by_my_KV, rng.nextInt(2000), theme),
-          _getDataRow(t.campaigns.statistic.by_my_LV, rng.nextInt(20000), theme),
-          _getDataRow(t.campaigns.statistic.in_germany, rng.nextInt(20000), theme),
+          _getDataRow(t.campaigns.statistic.by_me, stats.own.toInt(), theme),
+          _getDataRow(t.campaigns.statistic.by_my_KV, stats.division.toInt(), theme),
+          _getDataRow(t.campaigns.statistic.by_my_LV, stats.state.toInt(), theme),
+          _getDataRow(t.campaigns.statistic.in_germany, stats.germany.toInt(), theme),
         ],
       ),
     );
@@ -141,4 +264,9 @@ class StatisticsScreen extends StatelessWidget {
       ),
     );
   }
+
+  Future<CampaignStatistics> _loadStatistics() async {
+    var statApiService = GetIt.I<GrueneApiCampaignsStatisticsService>();
+    return await statApiService.getStatistics();
+  }
 }
diff --git a/lib/i18n/app_de.json b/lib/i18n/app_de.json
index c8abbdf6..048f37e0 100644
--- a/lib/i18n/app_de.json
+++ b/lib/i18n/app_de.json
@@ -113,6 +113,7 @@
     "statistic": {
       "label": "Statistik",
       "my_badges": "Meine Abzeichen",
+      "my_badges_campaign_subtitle": "Bundestagswahl 2025",
       "recorded_posters": "Plakate",
       "including_damaged_or_taken_down": "inkl. beschädigt und abgehängt",
       "recorded_doors": "Haustüren",
diff --git a/lib/main.dart b/lib/main.dart
index 10bafdc5..c27a5c0b 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -8,6 +8,7 @@ import 'package:get_it/get_it.dart';
 import 'package:gruene_app/app/auth/bloc/auth_bloc.dart';
 import 'package:gruene_app/app/auth/repository/auth_repository.dart';
 import 'package:gruene_app/app/router.dart';
+import 'package:gruene_app/app/services/gruene_api_campaigns_statistics_service.dart';
 import 'package:gruene_app/app/services/gruene_api_core.dart';
 import 'package:gruene_app/app/services/ip_service.dart';
 import 'package:gruene_app/app/services/nominatim_service.dart';
@@ -39,6 +40,7 @@ void main() async {
   }
 
   registerSecureStorage();
+  GetIt.I.registerSingleton<GrueneApiCampaignsStatisticsService>(GrueneApiCampaignsStatisticsService());
   GetIt.I.registerSingleton<AppSettings>(AppSettings());
   GetIt.I.registerFactory<AuthenticatorService>(MfaFactory.create);
   GetIt.I.registerSingleton<IpService>(IpService());
diff --git a/pubspec.yaml b/pubspec.yaml
index fc923012..ec193a75 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -83,6 +83,7 @@ dev_dependencies:
 flutter:
   uses-material-design: true
   assets:
+    - assets/badges/
     - assets/bottom_navigation/
     - assets/icons/
     - assets/splash/
diff --git a/swaggers/gruene-api.yaml b/swaggers/gruene-api.yaml
index 41f4a8e2..6c767d53 100644
--- a/swaggers/gruene-api.yaml
+++ b/swaggers/gruene-api.yaml
@@ -1050,6 +1050,137 @@ paths:
       security:
         - bearer: []
         - oauth2: []
+  /v1/campaigns/polling-stations:
+    post:
+      operationId: createPollingStation
+      summary: Create a new PollingStation
+      parameters: []
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/CreatePollingStation'
+      responses:
+        '201':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/PollingStation'
+        '401':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
+    get:
+      operationId: findPollingStations
+      summary: Find PollingStations
+      parameters:
+        - name: bbox
+          required: false
+          in: query
+          schema:
+            example: 47.695103,7.659684,53.670793, 14.229507
+            type: string
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/FindPollingStationsResponse'
+        '401':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
+  /v1/campaigns/polling-stations/{pollingStationId}:
+    get:
+      operationId: getPollingStation
+      summary: Get a PollingStation
+      parameters:
+        - name: pollingStationId
+          required: true
+          in: path
+          schema:
+            type: string
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/PollingStation'
+        '401':
+          description: ''
+        '404':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
+    put:
+      operationId: updatePollingStation
+      summary: Update a PollingStation
+      parameters:
+        - name: pollingStationId
+          required: true
+          in: path
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/UpdatePollingStation'
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/PollingStation'
+        '401':
+          description: ''
+        '404':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
+    delete:
+      operationId: deletePollingStation
+      summary: Delete a PollingStation
+      parameters:
+        - name: pollingStationId
+          required: true
+          in: path
+          schema:
+            type: string
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/PollingStation'
+        '401':
+          description: ''
+        '404':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
   /v1/campaigns/pois:
     post:
       operationId: createPoi
@@ -1295,6 +1426,148 @@ paths:
       security:
         - bearer: []
         - oauth2: []
+  /v1/campaigns/routes:
+    post:
+      operationId: createRoute
+      summary: Create a new Route
+      parameters: []
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/CreateRoute'
+      responses:
+        '201':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Route'
+        '401':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
+    get:
+      operationId: findRoutes
+      summary: Find Routes
+      parameters:
+        - name: type
+          required: false
+          in: query
+          description: filter by Route type
+          schema:
+            example: POSTER
+            enum:
+              - FLYER_SPOT
+              - POSTER
+              - HOUSE
+            type: string
+        - name: bbox
+          required: false
+          in: query
+          schema:
+            example: 47.695103,7.659684,53.670793, 14.229507
+            type: string
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/FindRoutesResponse'
+        '401':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
+  /v1/campaigns/routes/{routeId}:
+    get:
+      operationId: getRoute
+      summary: Get a Route
+      parameters:
+        - name: routeId
+          required: true
+          in: path
+          schema:
+            type: string
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Route'
+        '401':
+          description: ''
+        '404':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
+    put:
+      operationId: updateRoute
+      summary: Update a Route
+      parameters:
+        - name: routeId
+          required: true
+          in: path
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/UpdateRoute'
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Route'
+        '401':
+          description: ''
+        '404':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
+    delete:
+      operationId: deleteRoute
+      summary: Delete a Route
+      parameters:
+        - name: routeId
+          required: true
+          in: path
+          schema:
+            type: string
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Route'
+        '401':
+          description: ''
+        '404':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
   /v1/campaigns/experience-areas:
     post:
       operationId: createExperienceArea
@@ -1557,6 +1830,25 @@ paths:
       security:
         - bearer: []
         - oauth2: []
+  /v1/campaigns/statistics:
+    get:
+      operationId: getStatistics
+      summary: Get statistics
+      parameters: []
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Statistics'
+        '401':
+          description: ''
+      tags:
+        - campaigns
+      security:
+        - bearer: []
+        - oauth2: []
   /v1/news:
     get:
       operationId: findNews
@@ -1676,7 +1968,7 @@ tags: []
 servers:
   - url: https://api.gruene.de
     description: Production
-  - url: http://192.168.178.95:5000
+  - url: http://192.168.178.35:5000
     description: Development
 components:
   securitySchemes:
@@ -3043,6 +3335,84 @@ components:
         - zip
         - street
         - houseNumber
+    UpdatePollingStation:
+      type: object
+      properties:
+        coords:
+          description: Coordinates represented in GeoJSON [longitude, latitude]
+          example:
+            - 52.5297
+            - 13.4266
+          minItems: 2
+          maxItems: 2
+          type: array
+          items:
+            type: number
+        address:
+          $ref: '#/components/schemas/PoiAddress'
+      required:
+        - coords
+        - address
+    CreatePollingStation:
+      type: object
+      properties:
+        coords:
+          description: Coordinates represented in GeoJSON [longitude, latitude]
+          example:
+            - 52.5297
+            - 13.4266
+          minItems: 2
+          maxItems: 2
+          type: array
+          items:
+            type: number
+        address:
+          $ref: '#/components/schemas/PoiAddress'
+      required:
+        - coords
+        - address
+    PollingStation:
+      type: object
+      properties:
+        coords:
+          description: Coordinates represented in GeoJSON [longitude, latitude]
+          example:
+            - 52.5297
+            - 13.4266
+          minItems: 2
+          maxItems: 2
+          type: array
+          items:
+            type: number
+        id:
+          type: string
+          example: '1'
+        userId:
+          type: string
+        createdAt:
+          format: date-time
+          type: string
+        updatedAt:
+          format: date-time
+          type: string
+        address:
+          $ref: '#/components/schemas/PoiAddress'
+      required:
+        - coords
+        - id
+        - userId
+        - createdAt
+        - updatedAt
+        - address
+    FindPollingStationsResponse:
+      type: object
+      properties:
+        data:
+          type: array
+          items:
+            $ref: '#/components/schemas/PollingStation'
+      required:
+        - data
     PoiPoster:
       type: object
       properties:
@@ -3252,6 +3622,86 @@ components:
             $ref: '#/components/schemas/Poi'
       required:
         - data
+    LineString:
+      type: object
+      properties:
+        type:
+          type: string
+          description: Type of the LineString
+          example: LineString
+          default: LineString
+        coordinates:
+          type: array
+          items:
+            required: true
+            description: |-
+              Coordinates of the LineString
+              Must follow the GeoJSON standard
+            type: array
+            items:
+              type: number
+      required:
+        - type
+        - coordinates
+    UpdateRoute:
+      type: object
+      properties:
+        type:
+          enum:
+            - FLYER_SPOT
+            - POSTER
+            - HOUSE
+          type: string
+        lineString:
+          $ref: '#/components/schemas/LineString'
+      required:
+        - type
+        - lineString
+    CreateRoute:
+      type: object
+      properties:
+        type:
+          enum:
+            - FLYER_SPOT
+            - POSTER
+            - HOUSE
+          type: string
+        lineString:
+          $ref: '#/components/schemas/LineString'
+      required:
+        - type
+        - lineString
+    Route:
+      type: object
+      properties:
+        id:
+          type: string
+          example: '1'
+        type:
+          enum:
+            - FLYER_SPOT
+            - POSTER
+            - HOUSE
+          type: string
+        createdAt:
+          format: date-time
+          type: string
+        lineString:
+          $ref: '#/components/schemas/LineString'
+      required:
+        - id
+        - type
+        - createdAt
+        - lineString
+    FindRoutesResponse:
+      type: object
+      properties:
+        data:
+          type: array
+          items:
+            $ref: '#/components/schemas/Route'
+      required:
+        - data
     UpdateExperienceArea:
       type: object
       properties:
@@ -3385,6 +3835,35 @@ components:
             $ref: '#/components/schemas/FocusArea'
       required:
         - data
+    PoiStatistics:
+      type: object
+      properties:
+        own:
+          type: number
+        division:
+          type: number
+        state:
+          type: number
+        germany:
+          type: number
+      required:
+        - own
+        - division
+        - state
+        - germany
+    Statistics:
+      type: object
+      properties:
+        poster:
+          $ref: '#/components/schemas/PoiStatistics'
+        flyer:
+          $ref: '#/components/schemas/PoiStatistics'
+        house:
+          $ref: '#/components/schemas/PoiStatistics'
+      required:
+        - poster
+        - flyer
+        - house
     NewsCategory:
       type: object
       properties:

From 0ae6125e11bf144479a7c9d043b6cf736b31c3bb Mon Sep 17 00:00:00 2001
From: Christian Fiebrig <fiebrig@bitidee.de>
Date: Tue, 28 Jan 2025 18:57:24 +0100
Subject: [PATCH 3/3] 314: badges completed

- set margin between badges correctly
- store recent stats to only update every 5 minutes
---
 lib/app/services/converters.dart              |  1 +
 .../converters/date_time_parsing.dart         | 13 ++++++++++
 ...uene_api_campaigns_statistics_service.dart | 18 ++-----------
 .../helper/campaign_session_settings.dart     |  4 +++
 .../statistics/campaign_statistics.dart       | 11 ++++++++
 .../statistics/campaign_statistics_set.dart   | 10 ++++++++
 .../campaigns/screens/statistics_screen.dart  | 25 +++++++++++++++----
 lib/i18n/app_de.json                          |  3 ++-
 8 files changed, 63 insertions(+), 22 deletions(-)
 create mode 100644 lib/app/services/converters/date_time_parsing.dart
 create mode 100644 lib/features/campaigns/models/statistics/campaign_statistics.dart
 create mode 100644 lib/features/campaigns/models/statistics/campaign_statistics_set.dart

diff --git a/lib/app/services/converters.dart b/lib/app/services/converters.dart
index 6f7b7936..714afa38 100644
--- a/lib/app/services/converters.dart
+++ b/lib/app/services/converters.dart
@@ -28,3 +28,4 @@ part 'converters/poi_parsing.dart';
 part 'converters/slider_range_parsing.dart';
 part 'converters/place_parser.dart';
 part 'converters/string_extension.dart';
+part 'converters/date_time_parsing.dart';
diff --git a/lib/app/services/converters/date_time_parsing.dart b/lib/app/services/converters/date_time_parsing.dart
new file mode 100644
index 00000000..f6ca8f6f
--- /dev/null
+++ b/lib/app/services/converters/date_time_parsing.dart
@@ -0,0 +1,13 @@
+part of '../converters.dart';
+
+extension DateTimeParsing on DateTime {
+  String getAsLocalDateTimeString() {
+    DateTime utcDateTime = this;
+    DateTime localDateTime = utcDateTime.toLocal();
+    final dateString = DateFormat(t.campaigns.poster.date_format).format(localDateTime);
+    final timeString = DateFormat(t.campaigns.poster.time_format).format(localDateTime);
+    return t.campaigns.poster.datetime_display_template
+        .replaceAll('{date}', dateString)
+        .replaceAll('{time}', timeString);
+  }
+}
diff --git a/lib/app/services/gruene_api_campaigns_statistics_service.dart b/lib/app/services/gruene_api_campaigns_statistics_service.dart
index 21ff5de0..768d246f 100644
--- a/lib/app/services/gruene_api_campaigns_statistics_service.dart
+++ b/lib/app/services/gruene_api_campaigns_statistics_service.dart
@@ -1,5 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:get_it/get_it.dart';
+import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics.dart';
+import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_set.dart';
 import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart';
 
 class GrueneApiCampaignsStatisticsService {
@@ -41,19 +43,3 @@ extension PoiStatisticsParser on PoiStatistics {
     );
   }
 }
-
-class CampaignStatisticsSet {
-  final double own, division, state, germany;
-
-  const CampaignStatisticsSet({required this.own, required this.division, required this.state, required this.germany});
-}
-
-class CampaignStatistics {
-  final CampaignStatisticsSet flyerStats, houseStats, posterStats;
-
-  const CampaignStatistics({
-    required this.flyerStats,
-    required this.houseStats,
-    required this.posterStats,
-  });
-}
diff --git a/lib/features/campaigns/helper/campaign_session_settings.dart b/lib/features/campaigns/helper/campaign_session_settings.dart
index 0658dc42..5d4bbf1a 100644
--- a/lib/features/campaigns/helper/campaign_session_settings.dart
+++ b/lib/features/campaigns/helper/campaign_session_settings.dart
@@ -1,10 +1,14 @@
 import 'package:gruene_app/app/services/nominatim_service.dart';
+import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics.dart';
 import 'package:maplibre_gl/maplibre_gl.dart';
 
 class CampaignSessionSettings {
   LatLng? lastPosition;
   double? lastZoomLevel;
 
+  CampaignStatistics? recentStatistics;
+  DateTime? recentStatisticsFetchTimestamp;
+
   bool imageConsentConfirmed = false;
 
   String? searchString;
diff --git a/lib/features/campaigns/models/statistics/campaign_statistics.dart b/lib/features/campaigns/models/statistics/campaign_statistics.dart
new file mode 100644
index 00000000..766cb4b4
--- /dev/null
+++ b/lib/features/campaigns/models/statistics/campaign_statistics.dart
@@ -0,0 +1,11 @@
+import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_set.dart';
+
+class CampaignStatistics {
+  final CampaignStatisticsSet flyerStats, houseStats, posterStats;
+
+  const CampaignStatistics({
+    required this.flyerStats,
+    required this.houseStats,
+    required this.posterStats,
+  });
+}
diff --git a/lib/features/campaigns/models/statistics/campaign_statistics_set.dart b/lib/features/campaigns/models/statistics/campaign_statistics_set.dart
new file mode 100644
index 00000000..e4258364
--- /dev/null
+++ b/lib/features/campaigns/models/statistics/campaign_statistics_set.dart
@@ -0,0 +1,10 @@
+class CampaignStatisticsSet {
+  final double own, division, state, germany;
+
+  const CampaignStatisticsSet({
+    required this.own,
+    required this.division,
+    required this.state,
+    required this.germany,
+  });
+}
diff --git a/lib/features/campaigns/screens/statistics_screen.dart b/lib/features/campaigns/screens/statistics_screen.dart
index b7f7778e..836c3619 100644
--- a/lib/features/campaigns/screens/statistics_screen.dart
+++ b/lib/features/campaigns/screens/statistics_screen.dart
@@ -1,9 +1,13 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_svg/flutter_svg.dart';
 import 'package:get_it/get_it.dart';
+import 'package:gruene_app/app/services/converters.dart';
 import 'package:gruene_app/app/services/gruene_api_campaigns_statistics_service.dart';
 import 'package:gruene_app/app/theme/theme.dart';
+import 'package:gruene_app/features/campaigns/helper/app_settings.dart';
 import 'package:gruene_app/features/campaigns/helper/campaign_constants.dart';
+import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics.dart';
+import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_set.dart';
 import 'package:gruene_app/i18n/translations.g.dart';
 import 'package:intl/intl.dart';
 
@@ -26,6 +30,7 @@ class StatisticsScreen extends StatelessWidget {
   }
 
   SingleChildScrollView _buildStatScreen(CampaignStatistics statistics, ThemeData theme, BuildContext context) {
+    var lastUpdateTime = GetIt.I<AppSettings>().campaign.recentStatisticsFetchTimestamp ?? DateTime.now();
     return SingleChildScrollView(
       child: Container(
         padding: EdgeInsets.all(16),
@@ -57,7 +62,7 @@ class StatisticsScreen extends StatelessWidget {
               child: Align(
                 alignment: Alignment.centerLeft,
                 child: Text(
-                  'Stand: ${DateTime.now().toString()} (${t.campaigns.statistic.update_info})',
+                  '${t.campaigns.statistic.as_at}: ${lastUpdateTime.getAsLocalDateTimeString()} (${t.campaigns.statistic.update_info})',
                   style: theme.textTheme.labelMedium!.apply(color: ThemeColors.textDisabled),
                 ),
               ),
@@ -111,7 +116,6 @@ class StatisticsScreen extends StatelessWidget {
   }
 
   Widget _getBadgeRow(String title, int ownCounter, ThemeData theme) {
-    // var rng = Random();
     return Container(
       decoration: BoxDecoration(
         border: Border(
@@ -158,7 +162,7 @@ class StatisticsScreen extends StatelessWidget {
                     alignment: Alignment.center,
                     child: Text(
                       currentThreshold.toString(),
-                      style: theme.textTheme.labelMedium!.apply(fontWeightDelta: 3),
+                      style: theme.textTheme.labelMedium!.apply(fontWeightDelta: 3, fontStyle: FontStyle.italic),
                     ),
                   ),
                 ),
@@ -193,7 +197,7 @@ class StatisticsScreen extends StatelessWidget {
           ),
         );
       }
-      widgets.add(SizedBox(width: 6));
+      if (currentThreshold != thresholds.last) widgets.add(SizedBox(width: 5));
     }
     return widgets;
   }
@@ -266,7 +270,18 @@ class StatisticsScreen extends StatelessWidget {
   }
 
   Future<CampaignStatistics> _loadStatistics() async {
+    var campaignSettings = GetIt.I<AppSettings>().campaign;
+
+    if (campaignSettings.recentStatistics != null &&
+        DateTime.now().isBefore(campaignSettings.recentStatisticsFetchTimestamp!.add(Duration(minutes: 5)))) {
+      return campaignSettings.recentStatistics!;
+    }
     var statApiService = GetIt.I<GrueneApiCampaignsStatisticsService>();
-    return await statApiService.getStatistics();
+    var campaignStatistics = await statApiService.getStatistics();
+
+    campaignSettings.recentStatistics = campaignStatistics;
+    campaignSettings.recentStatisticsFetchTimestamp = DateTime.now();
+
+    return campaignStatistics;
   }
 }
diff --git a/lib/i18n/app_de.json b/lib/i18n/app_de.json
index 048f37e0..facd9b07 100644
--- a/lib/i18n/app_de.json
+++ b/lib/i18n/app_de.json
@@ -122,7 +122,8 @@
       "by_my_KV": "von meinen Kreisverband",
       "by_my_LV": "von meinem Landesverband",
       "in_germany": "deutschlandweit",
-      "update_info": "wird alle 5 Minuten aktualisiert"
+      "update_info": "wird alle 5 Minuten aktualisiert",
+      "as_at": "Stand"
     },
     "address": {
       "street": "Straße",