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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000..768d246f --- /dev/null +++ b/lib/app/services/gruene_api_campaigns_statistics_service.dart @@ -0,0 +1,45 @@ +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 { + late GrueneApi grueneApi; + + GrueneApiCampaignsStatisticsService() { + grueneApi = GetIt.I(); + } + + Future 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, + ); + } +} 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/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/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 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..836c3619 100644 --- a/lib/features/campaigns/screens/statistics_screen.dart +++ b/lib/features/campaigns/screens/statistics_screen.dart @@ -1,20 +1,287 @@ 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'; 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 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) { + var lastUpdateTime = GetIt.I().campaign.recentStatisticsFetchTimestamp ?? DateTime.now(); + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.all(16), + color: theme.colorScheme.surfaceDim, + child: Column( + children: [ + _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, + ), + Container( + padding: EdgeInsets.all(16), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + '${t.campaigns.statistic.as_at}: ${lastUpdateTime.getAsLocalDateTimeString()} (${t.campaigns.statistic.update_info})', + style: theme.textTheme.labelMedium!.apply(color: ThemeColors.textDisabled), + ), + ), + ), + ], + ), + ), + ); + } + + 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.background, + 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, + ), + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + t.campaigns.statistic.my_badges_campaign_subtitle, + style: theme.textTheme.labelSmall, + ), + ), + ..._getBadges(statistics, theme), + ], + ), + ); + } + + List _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 _getBadgeRow(String title, int ownCounter, ThemeData theme) { + 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 _getBadgeIcons(int value, ThemeData theme) { + var thresholds = [50, 100, 250, 500]; + var badges = ['bronze', 'silver', 'gold', 'platinum']; + var widgets = []; + 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, fontStyle: FontStyle.italic), + ), + ), + ), + ], + ), + ), + ); + } 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)), + ), + ), + ), + ], + ), + ), + ); + } + if (currentThreshold != thresholds.last) widgets.add(SizedBox(width: 5)); + } + 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), + boxShadow: [ + BoxShadow(color: ThemeColors.textDark.withAlpha(10), offset: Offset(2, 4)), + ], + ); + 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, 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), + ], + ), + ); + } + + 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), + ), + ], + ), + ); + } + + Future _loadStatistics() async { + var campaignSettings = GetIt.I().campaign; + + if (campaignSettings.recentStatistics != null && + DateTime.now().isBefore(campaignSettings.recentStatisticsFetchTimestamp!.add(Duration(minutes: 5)))) { + return campaignSettings.recentStatistics!; + } + var statApiService = GetIt.I(); + 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 7cf0db93..facd9b07 100644 --- a/lib/i18n/app_de.json +++ b/lib/i18n/app_de.json @@ -111,7 +111,19 @@ "label": "Team" }, "statistic": { - "label": "Statistik" + "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", + "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", + "as_at": "Stand" }, "address": { "street": "Straße", 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()); GetIt.I.registerSingleton(AppSettings()); GetIt.I.registerFactory(MfaFactory.create); GetIt.I.registerSingleton(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: