From 432a2ab534480caa86fc4c733a4d9ec6fc56be36 Mon Sep 17 00:00:00 2001 From: Christian Fiebrig Date: Wed, 5 Feb 2025 09:36:44 +0100 Subject: [PATCH] 575 enhancements for posters (#577) * 550: add new poster_icons * 575: fix statistics interface * 546: new poster status "to be moved" * 550: update poster icons * 564: show detail bottom sheet * 564: set focus on map element * 574: change edit view to show new state selection --- assets/symbols/posters/poster.png | Bin 540 -> 418 bytes assets/symbols/posters/poster_damaged.png | Bin 536 -> 394 bytes assets/symbols/posters/poster_missing.png | Bin 0 -> 419 bytes assets/symbols/posters/poster_removed.png | Bin 540 -> 484 bytes assets/symbols/posters/poster_to_be_moved.png | Bin 0 -> 462 bytes .../posters/svg_inverted/poster_damaged.svg | 4 + .../posters/svg_inverted/poster_missing.svg | 3 + .../posters/svg_inverted/poster_ok.svg | 3 + .../posters/svg_inverted/poster_removed.svg | 3 + .../posters/svg_inverted/poster_tobemoved.svg | 3 + .../converters/poi_poster_status_parsing.dart | 2 + .../converters/poster_status_parsing.dart | 1 + ...uene_api_campaigns_statistics_service.dart | 10 +- .../campaigns/helper/campaign_constants.dart | 2 + .../helper/campaign_session_settings.dart | 4 +- lib/features/campaigns/helper/enums.dart | 2 + .../campaigns/helper/poster_status.dart | 13 +- .../models/posters/poster_detail_model.dart | 2 + ...cs.dart => campaign_statistics_model.dart} | 4 +- .../campaigns/screens/map_consumer.dart | 44 ++- .../campaigns/screens/poster_detail.dart | 131 +++++-- .../campaigns/screens/poster_edit.dart | 118 ++---- .../campaigns/screens/posters_screen.dart | 4 +- .../campaigns/screens/statistics_screen.dart | 10 +- .../campaigns/widgets/close_edit_widget.dart | 41 +++ .../campaigns/widgets/map_container.dart | 72 +++- .../campaigns/widgets/map_controller.dart | 3 + lib/i18n/app_de.json | 16 +- pubspec.yaml | 1 + swaggers/gruene-api.yaml | 344 ++++++++++-------- 30 files changed, 535 insertions(+), 305 deletions(-) create mode 100644 assets/symbols/posters/poster_missing.png create mode 100644 assets/symbols/posters/poster_to_be_moved.png create mode 100644 assets/symbols/posters/svg_inverted/poster_damaged.svg create mode 100644 assets/symbols/posters/svg_inverted/poster_missing.svg create mode 100644 assets/symbols/posters/svg_inverted/poster_ok.svg create mode 100644 assets/symbols/posters/svg_inverted/poster_removed.svg create mode 100644 assets/symbols/posters/svg_inverted/poster_tobemoved.svg rename lib/features/campaigns/models/statistics/{campaign_statistics.dart => campaign_statistics_model.dart} (79%) create mode 100644 lib/features/campaigns/widgets/close_edit_widget.dart diff --git a/assets/symbols/posters/poster.png b/assets/symbols/posters/poster.png index 0c2771e974115abd63b32854464335c7e7118862..cc3cc889031bce8d2c0fff46d2d66b43de796710 100644 GIT binary patch delta 398 zcmV;90dfAE1fl~riBL{Q4GJ0x0000DNk~Le0000O0000W2nGNE00>^VH~;_u32;bR za{vGqB>(^xB>_oNB=C_TAAbQiNklIXu9a+0FL4St4@A` z5^?h@nDk6cT=@b>-E?wu6%%nXft`T@0|^U|urM9(+i*adl=R!(ek8dgq```{#eVGnBZ2UZxLuGmMF{)qB_!)H&1?%7l_J8>e#8r)NU6Q~s zjJIGvgJ;kEi>{6Cq=&dTmo+6}szFJP2j>uXa8bJRuTOssh~I{K>;|_hhlDt#`$&?+ zf~b-_Qo<5Ov|oLRpSLf5kZJ|HlHHZm|35ZOi3P1;CtT9rY*iug7FFIKLfdA`g^Fe- zTeJ@?&;l*c0xi%2B~O@fC?6M*6@|ky6+ITzIW?~zso@l9DW6>6^Y%sEw)p61Qs6JR sg>3PtDt=m@u$vj2vKjsbaMlyY3Vi@N0#BbA;Q#;t07*qoM6N<$g5cn#M*si- literal 540 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy`!3-qNPSte*QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`T?2eVToV!!-oJnU|NsB{_wUEY$1`+VIxNqwKQbp_YxDfeTi4$@7{9ey zVMfsX56{-GUw{7md4u_J`}gnfKfU7pk5BXG&+qT=2b#oQvh_cZ5-$nz3kDj61u#5w zcoqj#!dc)ESDq`h1PS#u?GuqR|F+?Ny=|yk81_d6L3r0VjkM7_5KTx-Y^^E8o z*UNiKt_l_|_~BO*x!QuUitB^3%&|Fa%Ren)V}4d5!6807`^d43ZQ6-uhm;?M9D02$ ld09xB+0<2j?dB6-u|N63`E~ws`K=%;Jzf1=);T3K0RUmLtEd0~ diff --git a/assets/symbols/posters/poster_damaged.png b/assets/symbols/posters/poster_damaged.png index c250990fed11b23aaf026da57337538f0a525748..bee1ee97d006a3c6495b9b91a1495e456b23bc0a 100644 GIT binary patch delta 374 zcmV-+0g3*Y1d0PTiBL{Q4GJ0x0000DNk~Le0000O0000W2nGNE00>^VH~;_u32;bR za{vGqB>(^xB>_oNB=C_TAAbQKNkl7HTtnDG7ZPy97Y4~v-psLoB>Kb>&jq?B`DfJ{N-7{M!T#o0*c>q$^ zx$!h1CW$tCKK1>VaXw(FZH04%gdOJqMX{b#mis@H8|MM&p%8jZ{ZUEe#O(rX(dHKS zqxVLkCjSRuoJUy(mp7>>>}aIlt=AlFaoQl974+#;w>diAd-x?8h`{TP@4}kD0Be`F Ul%?orp8x;=07*qoM6N<$f&)pJxBvhE literal 536 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy`!3-qNPSte*QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`odSG9ToV!!-oJnU|NsB{_wUEY$N%??{a@bpf62Q4y|e!BKJ@?Cng0oe z|6jabzkdDs^XLC(SMT4y|Nr_O|G#{lKYxCIe?QO|$*`d5KuWA6$S)XZ4kln=yma6R zPz7g!M`SSr1K%MKW)#)%Y5;3L5h);ff2~^0%B<>8{~HlMrN=$6Oe7l$i%=0q@#d1vz-Mjo&jWo zz^5`sh8Msfg3%~eG5{qeursg#)fpHW8!#?_m$Nv;eZ8 zx(p2rK(ew@5i8GevgQJr(Vi}jAsWF?FM9GFP~c%ckg&_a`}?#1@wzTD2jW-o2b!GH z6f0ct!?7lIwgsab*9Yf0Y2NJ%?N%P{SSsUoXu|VZ8w}HKPrcCd!NX$ehg~02g0(LD gEM0l|q3)7jjMsN_9$u<^M;2tIr>mdKI;Vst06vPY-v9sr diff --git a/assets/symbols/posters/poster_missing.png b/assets/symbols/posters/poster_missing.png new file mode 100644 index 0000000000000000000000000000000000000000..3de3a132ed763c2dd3e2d5120b412ace4da61ff3 GIT binary patch literal 419 zcmV;U0bKrxP)Bc*7#;6C!n0&e#dg@5cM z0f*gAJXY>x!h|C|>@1?)Qxp&caf-_FouaE(m0T!dWqswSeIxbuM60C^Ec;&lx^VH~;_u32;bR za{vGqB>(^xB>_oNB=C_TAAbRPNkl(CDjehe~;ElUuOR2QHkF^nLPi z@67=S(=-(t$28hzc-nTB0$2F0DId;MqU1QDk?ntLxNJ1>`u2`6On)m=D6Z#`ipR6` z^KgQ1iHgkW`2{|DpOC@)+${FDHn>zH9L|-jaOUOrDP$rTGGoDr3uvx;o)cIwLkp_3 zQ$TKI8PWwD&I@TW7DFNtmn(3GKR$O*zi9>+r0DA0k3ck>iT-tPdo6E;Ch289w4Q{+ zJ@24SyQ^*(SjwbPDt{J)!#xFE+5oGyD@($NhBHyG6CBCnLK@e1_YlQX;7GbX@o-Op zpOwn=H4VaoBu&wSyqNn{1`T?2eVToV!!-oJnU|NsB{_wUEY$In^4e(Qn5XRlsAc>3(E2M-@SeRlBF z*_GRNe*XG({rdIi&!1nvd*A;3`>))(^YhoQ`Sa)Z_xA%$nzDBO8XzTJ666;QGztq~ zc;@gd4yc5)z$3Dlfr0N32s4Umcr^e8#Y$WwN`mv#O3D+9QW*jgGxJLH{9Hp6O!W-E zH-DcDRIw#B!ZXd&Q;UHE$YEuWVq|4t1hTw}~1u&RkG>VlBK#2+L3@ku(21dpPj0+%Ug6v~m05NA0kPQM%KvS8( zDuXO7fGnskLjwbltgKYT%5$8oxj<&Lr;B5VM)1>%-h2%TJS-QCemEc9zxRKjZVT%f z(K)V{_mo@}EL`xzuO@P}1!EQ02WOdMbJ&)DTEfQstVDuCe0KJcV;S4D6U`1OKMFbY n`dIR^kTkQYtNhx{C%$5T@`dy3{N?gnK~{RY`njxgN@xNAXC1TM diff --git a/assets/symbols/posters/poster_to_be_moved.png b/assets/symbols/posters/poster_to_be_moved.png new file mode 100644 index 0000000000000000000000000000000000000000..1ab347f57bc4e5b078a76901a36cf9f98b6fab5c GIT binary patch literal 462 zcmV;<0WtoGP)nC66ipzK&?PK$(3RUmOu&E0d$}pyvyc;k+IHHm3cq@H~1(u5Bg2Z}?6Q|5>3_u;hv@{p7R zcpM_|{PAeW0AD04XqX*R>jUX7Rw|s(ZfC|~MX`FcTEMjw@+^TA@}wl^BAVnvc#FWO z-;ml;_^nMNPD4)CD{;F+IK8XmYcIcm6_H%-`Ys*$54sm0b{NJHF#rGn07*qoM6N<$ Ef@QkKv;Y7A literal 0 HcmV?d00001 diff --git a/assets/symbols/posters/svg_inverted/poster_damaged.svg b/assets/symbols/posters/svg_inverted/poster_damaged.svg new file mode 100644 index 00000000..3e7c6995 --- /dev/null +++ b/assets/symbols/posters/svg_inverted/poster_damaged.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/symbols/posters/svg_inverted/poster_missing.svg b/assets/symbols/posters/svg_inverted/poster_missing.svg new file mode 100644 index 00000000..1f01d088 --- /dev/null +++ b/assets/symbols/posters/svg_inverted/poster_missing.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/symbols/posters/svg_inverted/poster_ok.svg b/assets/symbols/posters/svg_inverted/poster_ok.svg new file mode 100644 index 00000000..2b40be69 --- /dev/null +++ b/assets/symbols/posters/svg_inverted/poster_ok.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/symbols/posters/svg_inverted/poster_removed.svg b/assets/symbols/posters/svg_inverted/poster_removed.svg new file mode 100644 index 00000000..5135581e --- /dev/null +++ b/assets/symbols/posters/svg_inverted/poster_removed.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/symbols/posters/svg_inverted/poster_tobemoved.svg b/assets/symbols/posters/svg_inverted/poster_tobemoved.svg new file mode 100644 index 00000000..9c136533 --- /dev/null +++ b/assets/symbols/posters/svg_inverted/poster_tobemoved.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/app/services/converters/poi_poster_status_parsing.dart b/lib/app/services/converters/poi_poster_status_parsing.dart index 371f6cb8..6c66605e 100644 --- a/lib/app/services/converters/poi_poster_status_parsing.dart +++ b/lib/app/services/converters/poi_poster_status_parsing.dart @@ -7,6 +7,7 @@ extension PoiPosterStatusParsing on PoiPosterStatus { PoiPosterStatus.damaged => PosterStatus.damaged, PoiPosterStatus.missing => PosterStatus.missing, PoiPosterStatus.removed => PosterStatus.removed, + PoiPosterStatus.toBeMoved => PosterStatus.toBeMoved, PoiPosterStatus.swaggerGeneratedUnknown => throw UnimplementedError(), }; } @@ -17,6 +18,7 @@ extension PoiPosterStatusParsing on PoiPosterStatus { PoiPosterStatus.damaged => t.campaigns.poster.status.damaged.label, PoiPosterStatus.removed => t.campaigns.poster.status.removed.label, PoiPosterStatus.missing => t.campaigns.poster.status.missing.label, + PoiPosterStatus.toBeMoved => t.campaigns.poster.status.to_be_moved.label, PoiPosterStatus.swaggerGeneratedUnknown => throw UnimplementedError(), }; } diff --git a/lib/app/services/converters/poster_status_parsing.dart b/lib/app/services/converters/poster_status_parsing.dart index 29a73f14..bf73b635 100644 --- a/lib/app/services/converters/poster_status_parsing.dart +++ b/lib/app/services/converters/poster_status_parsing.dart @@ -7,6 +7,7 @@ extension PosterStatusParsing on PosterStatus { PosterStatus.damaged => PoiPosterStatus.damaged, PosterStatus.missing => PoiPosterStatus.missing, PosterStatus.removed => PoiPosterStatus.removed, + PosterStatus.toBeMoved => PoiPosterStatus.toBeMoved, }; } diff --git a/lib/app/services/gruene_api_campaigns_statistics_service.dart b/lib/app/services/gruene_api_campaigns_statistics_service.dart index 768d246f..515e854a 100644 --- a/lib/app/services/gruene_api_campaigns_statistics_service.dart +++ b/lib/app/services/gruene_api_campaigns_statistics_service.dart @@ -1,6 +1,6 @@ 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_model.dart'; import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_set.dart'; import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; @@ -11,7 +11,7 @@ class GrueneApiCampaignsStatisticsService { grueneApi = GetIt.I(); } - Future getStatistics() async { + Future getStatistics() async { try { var statResult = await grueneApi.v1CampaignsStatisticsGet(); return statResult.body!.asCampaignStatistics(); @@ -23,9 +23,9 @@ class GrueneApiCampaignsStatisticsService { } } -extension StatisticsParser on Statistics { - CampaignStatistics asCampaignStatistics() { - return CampaignStatistics( +extension StatisticsParser on CampaignStatistics { + CampaignStatisticsModel asCampaignStatistics() { + return CampaignStatisticsModel( flyerStats: flyer.asStatisticsSet(), houseStats: house.asStatisticsSet(), posterStats: poster.asStatisticsSet(), diff --git a/lib/features/campaigns/helper/campaign_constants.dart b/lib/features/campaigns/helper/campaign_constants.dart index 1e6c0051..a1c7f0da 100644 --- a/lib/features/campaigns/helper/campaign_constants.dart +++ b/lib/features/campaigns/helper/campaign_constants.dart @@ -6,7 +6,9 @@ class CampaignConstants { static const flyerAssetName = 'assets/symbols/flyer/flyer.png'; static const posterOkAssetName = 'assets/symbols/posters/poster.png'; static const posterDamagedAssetName = 'assets/symbols/posters/poster_damaged.png'; + static const posterMissingAssetName = 'assets/symbols/posters/poster_missing.png'; static const posterRemovedAssetName = 'assets/symbols/posters/poster_removed.png'; + static const posterToBeMovedAssetName = 'assets/symbols/posters/poster_to_be_moved.png'; static const addMarkerAssetName = 'assets/symbols/add_marker.svg'; static const markerSourceName = 'markers'; diff --git a/lib/features/campaigns/helper/campaign_session_settings.dart b/lib/features/campaigns/helper/campaign_session_settings.dart index 5d4bbf1a..488149b0 100644 --- a/lib/features/campaigns/helper/campaign_session_settings.dart +++ b/lib/features/campaigns/helper/campaign_session_settings.dart @@ -1,12 +1,12 @@ import 'package:gruene_app/app/services/nominatim_service.dart'; -import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics.dart'; +import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_model.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; class CampaignSessionSettings { LatLng? lastPosition; double? lastZoomLevel; - CampaignStatistics? recentStatistics; + CampaignStatisticsModel? recentStatistics; DateTime? recentStatisticsFetchTimestamp; bool imageConsentConfirmed = false; diff --git a/lib/features/campaigns/helper/enums.dart b/lib/features/campaigns/helper/enums.dart index 760a24bf..88571490 100644 --- a/lib/features/campaigns/helper/enums.dart +++ b/lib/features/campaigns/helper/enums.dart @@ -1,3 +1,5 @@ enum ModalEditResult { cancel, save, delete } +enum ModalDetailResult { close, edit } + enum ImageType { jpeg, png } diff --git a/lib/features/campaigns/helper/poster_status.dart b/lib/features/campaigns/helper/poster_status.dart index 7cd3af28..c80100eb 100644 --- a/lib/features/campaigns/helper/poster_status.dart +++ b/lib/features/campaigns/helper/poster_status.dart @@ -2,21 +2,26 @@ import 'package:gruene_app/features/campaigns/models/posters/poster_detail_model import 'package:gruene_app/i18n/translations.g.dart'; class PosterStatusHelper { - static List<(PosterStatus, String, String)> getPosterStatusOptions = <(PosterStatus, String, String)>[ + static List<(PosterStatus, String)> getPosterStatusList = <(PosterStatus status, String label)>[ + ( + PosterStatus.ok, + t.campaigns.poster.status.ok.label, + ), ( PosterStatus.damaged, t.campaigns.poster.status.damaged.label, - t.campaigns.poster.status.damaged.hint, ), ( PosterStatus.missing, t.campaigns.poster.status.missing.label, - t.campaigns.poster.status.missing.hint, + ), + ( + PosterStatus.toBeMoved, + t.campaigns.poster.status.to_be_moved.label, ), ( PosterStatus.removed, t.campaigns.poster.status.removed.label, - t.campaigns.poster.status.removed.hint, ), ]; } diff --git a/lib/features/campaigns/models/posters/poster_detail_model.dart b/lib/features/campaigns/models/posters/poster_detail_model.dart index 7bcbd5cc..414f1b04 100644 --- a/lib/features/campaigns/models/posters/poster_detail_model.dart +++ b/lib/features/campaigns/models/posters/poster_detail_model.dart @@ -15,6 +15,8 @@ enum PosterStatus { missing, @JsonValue(400) removed, + @JsonValue(500) + toBeMoved, } @JsonSerializable() diff --git a/lib/features/campaigns/models/statistics/campaign_statistics.dart b/lib/features/campaigns/models/statistics/campaign_statistics_model.dart similarity index 79% rename from lib/features/campaigns/models/statistics/campaign_statistics.dart rename to lib/features/campaigns/models/statistics/campaign_statistics_model.dart index 766cb4b4..1ea65652 100644 --- a/lib/features/campaigns/models/statistics/campaign_statistics.dart +++ b/lib/features/campaigns/models/statistics/campaign_statistics_model.dart @@ -1,9 +1,9 @@ import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_set.dart'; -class CampaignStatistics { +class CampaignStatisticsModel { final CampaignStatisticsSet flyerStats, houseStats, posterStats; - const CampaignStatistics({ + const CampaignStatisticsModel({ required this.flyerStats, required this.houseStats, required this.posterStats, diff --git a/lib/features/campaigns/screens/map_consumer.dart b/lib/features/campaigns/screens/map_consumer.dart index 11d19e0f..365a8cf7 100644 --- a/lib/features/campaigns/screens/map_consumer.dart +++ b/lib/features/campaigns/screens/map_consumer.dart @@ -114,22 +114,44 @@ abstract class MapConsumer getPoiDetail, GetPoiEditWidgetCallback getPoiEdit, { Size desiredSize = const Size(100, 100), + bool useBottomSheet = false, }) async { final feature = rawFeature as Map; final poiId = MapHelper.extractPoiIdFromFeature(feature); U poi = await getPoi(poiId); final poiDetailWidget = getPoiDetail(poi); - var popupWidget = SizedBox( - height: desiredSize.height, - width: desiredSize.width, - child: poiDetailWidget, - ); - final coord = MapHelper.extractLatLngFromFeature(feature); - mapController.showMapPopover( - coord, - popupWidget, - () => _editPoi(() => getPoiEdit(poi)), - desiredSize, + if (useBottomSheet) { + await mapController.setFocusToMarkerItem(rawFeature); + var result = await showDetailBottomSheet(poiDetailWidget); + if (result != null && result == ModalDetailResult.edit) { + _editPoi(() => getPoiEdit(poi)); + } + await mapController.unsetFocusToMarkerItem(); + } else { + var popupWidget = SizedBox( + height: desiredSize.height, + width: desiredSize.width, + child: poiDetailWidget, + ); + final coord = MapHelper.extractLatLngFromFeature(feature); + mapController.showMapPopover( + coord, + popupWidget, + () => _editPoi(() => getPoiEdit(poi)), + desiredSize, + ); + } + } + + Future showDetailBottomSheet(Widget poiDetailWidget) async { + final theme = Theme.of(context); + return await showModalBottomSheet( + isScrollControlled: false, + isDismissible: true, + barrierColor: Colors.transparent, + context: context, + backgroundColor: theme.colorScheme.surface, + builder: (context) => poiDetailWidget, ); } diff --git a/lib/features/campaigns/screens/poster_detail.dart b/lib/features/campaigns/screens/poster_detail.dart index 4778d070..b784c884 100644 --- a/lib/features/campaigns/screens/poster_detail.dart +++ b/lib/features/campaigns/screens/poster_detail.dart @@ -1,10 +1,14 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:gruene_app/app/services/converters.dart'; +import 'package:gruene_app/app/theme/theme.dart'; import 'package:gruene_app/features/campaigns/helper/campaign_constants.dart'; +import 'package:gruene_app/features/campaigns/helper/enums.dart'; import 'package:gruene_app/features/campaigns/models/posters/poster_detail_model.dart'; -import 'package:gruene_app/features/campaigns/widgets/address_field_detail.dart'; +import 'package:gruene_app/features/campaigns/widgets/close_edit_widget.dart'; +import 'package:gruene_app/i18n/translations.g.dart'; class PosterDetail extends StatelessWidget { final PosterDetailModel poi; @@ -13,36 +17,105 @@ class PosterDetail extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - AddressFieldDetail( - street: poi.address.street, - houseNumber: poi.address.houseNumber, - ), - Expanded( - child: FutureBuilder( - future: Future.delayed( - Duration.zero, - () => poi.thumbnailUrl == null ? null : (thumbnailUrl: poi.thumbnailUrl), + getStatusColor() { + switch (poi.status) { + case PosterStatus.ok: + return ThemeColors.secondary; + case PosterStatus.damaged: + case PosterStatus.missing: + case PosterStatus.toBeMoved: + return Colors.red; + case PosterStatus.removed: + return ThemeColors.textDisabled; + } + } + + getStatusText() { + return switch (poi.status) { + PosterStatus.ok => t.campaigns.poster.status.ok.description, + PosterStatus.damaged => t.campaigns.poster.status.damaged.description, + PosterStatus.missing => t.campaigns.poster.status.missing.description, + PosterStatus.toBeMoved => t.campaigns.poster.status.to_be_moved.description, + PosterStatus.removed => t.campaigns.poster.status.removed.description, + }; + } + + var theme = Theme.of(context); + return GestureDetector( + onTap: () => _closeDialog(context, result: ModalDetailResult.edit), + child: SizedBox( + height: 250, + child: Column( + children: [ + Container( + padding: EdgeInsets.all(16), + child: CloseEditWidget( + onClose: () => _closeDialog(context), + onEdit: () => _closeDialog(context, result: ModalDetailResult.edit), + ), + ), + Row( + children: [ + Container( + padding: EdgeInsets.only(left: 12, bottom: 12), + height: 150, + width: 120, + child: FutureBuilder( + future: Future.delayed( + Duration.zero, + () => poi.thumbnailUrl == null ? null : (thumbnailUrl: poi.thumbnailUrl), + ), + builder: (context, snapshot) { + if (!snapshot.hasData && !snapshot.hasError) { + return Image.asset(CampaignConstants.dummyImageAssetName); + } + if (snapshot.data!.thumbnailUrl!.isNetworkImageUrl()) { + return FadeInImage.assetNetwork( + placeholder: CampaignConstants.dummyImageAssetName, + image: snapshot.data!.thumbnailUrl!, + ); + } else { + return Image.file( + File(snapshot.data!.thumbnailUrl!), + ); + } + }, + ), + ), + SizedBox(width: 12), + Text( + '${poi.address.street} ${poi.address.houseNumber}\n${poi.address.zipCode} ${poi.address.city}', + style: theme.textTheme.labelLarge!.copyWith(color: ThemeColors.text), + ), + ], ), - builder: (context, snapshot) { - if (!snapshot.hasData && !snapshot.hasError) { - return Image.asset(CampaignConstants.dummyImageAssetName); - } - if (snapshot.data!.thumbnailUrl!.isNetworkImageUrl()) { - return FadeInImage.assetNetwork( - placeholder: CampaignConstants.dummyImageAssetName, - image: snapshot.data!.thumbnailUrl!, - ); - } else { - return Image.file( - File(snapshot.data!.thumbnailUrl!), - ); - } - }, - ), + Expanded( + child: Container( + color: getStatusColor(), + padding: EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox(width: 12), + SvgPicture.asset( + 'assets/symbols/posters/svg_inverted/poster_${poi.status.name.toLowerCase()}.svg', + height: 18, + ), + SizedBox(width: 12), + Text( + getStatusText(), + style: theme.textTheme.labelLarge!.copyWith(color: ThemeColors.background), + ), + ], + ), + ), + ), + ], ), - ], + ), ); } + + void _closeDialog(BuildContext context, {ModalDetailResult result = ModalDetailResult.close}) { + Navigator.maybePop(context, result); + } } diff --git a/lib/features/campaigns/screens/poster_edit.dart b/lib/features/campaigns/screens/poster_edit.dart index 74d5be88..de02689c 100644 --- a/lib/features/campaigns/screens/poster_edit.dart +++ b/lib/features/campaigns/screens/poster_edit.dart @@ -37,8 +37,6 @@ class PosterEdit extends StatefulWidget { } class _PosterEditState extends State with AddressExtension, ConfirmDelete { - Set _segmentedButtonSelection = {}; - @override TextEditingController streetTextController = TextEditingController(); @override @@ -65,8 +63,8 @@ class _PosterEditState extends State with AddressExtension, ConfirmD void initState() { setAddress(widget.poster.address); commentTextController.text = widget.poster.comment; - if (widget.poster.status != PosterStatus.ok) _segmentedButtonSelection = {widget.poster.status}; _isPhotoDeleted = (widget.poster.imageUrl == null); + _selectedPosterStatus = widget.poster.status; super.initState(); } @@ -74,7 +72,6 @@ class _PosterEditState extends State with AddressExtension, ConfirmD @override Widget build(BuildContext context) { final theme = Theme.of(context); - final buttonStyle = _getSegmentedButtonStyle(theme); final currentSize = MediaQuery.of(context).size; final lightBorderColor = ThemeColors.textLight; var imageRowHeight = 130.0; @@ -218,51 +215,16 @@ class _PosterEditState extends State with AddressExtension, ConfirmD ), Container( padding: EdgeInsets.symmetric(vertical: 6), - child: Row( + height: 217, + child: Column( children: [ - Expanded( - child: Column( - children: [ - Align( - alignment: Alignment.center, - child: SegmentedButton( - multiSelectionEnabled: false, - emptySelectionAllowed: true, - showSelectedIcon: false, - selected: _segmentedButtonSelection, - onSelectionChanged: (Set newSelection) { - setState(() { - _segmentedButtonSelection = newSelection; - }); - }, - segments: PosterStatusHelper.getPosterStatusOptions.map>( - ((PosterStatus, String, String) posterStatusContext) { - return ButtonSegment( - value: posterStatusContext.$1, - label: Text(posterStatusContext.$2), - ); - }).toList(), - style: buttonStyle, - ), - ), - SizedBox( - height: 25, - child: Text( - _getCurrentPosterStatusHint(), - style: theme.textTheme.labelMedium!.apply( - color: ThemeColors.textDisabled, - ), - ), - ), - ], - ), - ), + ...PosterStatusHelper.getPosterStatusList.map(_getRadioItem), ], ), ), Container( padding: EdgeInsets.symmetric(vertical: 6), - height: 140, + height: 148, child: Row( children: [ Expanded( @@ -375,7 +337,7 @@ class _PosterEditState extends State with AddressExtension, ConfirmD final updateModel = PosterUpdateModel( id: widget.poster.id, address: getAddress(), - status: _segmentedButtonSelection.isEmpty ? PosterStatus.ok : _segmentedButtonSelection.single, + status: _selectedPosterStatus, comment: commentTextController.text, removePreviousPhotos: _isPhotoDeleted, location: widget.poster.location, @@ -387,54 +349,6 @@ class _PosterEditState extends State with AddressExtension, ConfirmD _closeDialog(ModalEditResult.save); } - ButtonStyle _getSegmentedButtonStyle(ThemeData theme) { - final WidgetStateProperty segmentedButtonBackgroundColor = WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.selected)) { - return ThemeColors.secondary; - } - return ThemeColors.background; - }, - ); - final WidgetStateProperty segmentedButtonForegroundColor = WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.selected)) { - return ThemeColors.background; - } - return ThemeColors.text; - }, - ); - - final WidgetStateProperty segmentedButtonTextStyle = WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.selected)) { - return theme.textTheme.labelMedium!.apply( - color: Colors.red, - ); - } - return theme.textTheme.labelMedium; - }, - ); - - return ButtonStyle( - textStyle: segmentedButtonTextStyle, //WidgetStatePropertyAll(theme.textTheme.labelMedium), - backgroundColor: segmentedButtonBackgroundColor, - foregroundColor: segmentedButtonForegroundColor, - side: WidgetStatePropertyAll(BorderSide(color: ThemeColors.secondary)), - ); - } - - String _getCurrentPosterStatusHint() { - if (_segmentedButtonSelection.isEmpty) return ''; - return _segmentedButtonSelection.map( - (selected) { - return PosterStatusHelper.getPosterStatusOptions - .firstWhere(((PosterStatus, String, String) posterStatusContext) => posterStatusContext.$1 == selected) - .$3; - }, - ).join('; '); - } - void _acquireNewPhoto() async { final photo = await MediaHelper.acquirePhoto(context); @@ -491,4 +405,24 @@ class _PosterEditState extends State with AddressExtension, ConfirmD } bool get _hasPhoto => _currentPhoto != null || (widget.poster.imageUrl != null && !_isPhotoDeleted); + + PosterStatus _selectedPosterStatus = PosterStatus.ok; + + Widget _getRadioItem((PosterStatus, String) item) { + var theme = Theme.of(context); + return RadioListTile( + value: item.$1, + groupValue: _selectedPosterStatus, + onChanged: (value) => setState(() { + _selectedPosterStatus = value!; + }), + fillColor: WidgetStatePropertyAll(ThemeColors.primary), + title: Text( + item.$2, + style: theme.textTheme.bodyMedium, + ), + visualDensity: VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + } } diff --git a/lib/features/campaigns/screens/posters_screen.dart b/lib/features/campaigns/screens/posters_screen.dart index 21ac1588..77c581de 100644 --- a/lib/features/campaigns/screens/posters_screen.dart +++ b/lib/features/campaigns/screens/posters_screen.dart @@ -152,8 +152,9 @@ class _PostersScreenState extends MapConsumer().campaign.recentStatisticsFetchTimestamp ?? DateTime.now(); return SingleChildScrollView( child: Container( @@ -73,7 +73,7 @@ class StatisticsScreen extends StatelessWidget { ); } - Widget _getBadgeBox(CampaignStatistics statistics, BuildContext context, ThemeData theme) { + Widget _getBadgeBox(CampaignStatisticsModel statistics, BuildContext context, ThemeData theme) { var mediaQuery = MediaQuery.of(context); return Container( padding: EdgeInsets.all(16), @@ -107,7 +107,7 @@ class StatisticsScreen extends StatelessWidget { ); } - List _getBadges(CampaignStatistics statistics, ThemeData theme) { + List _getBadges(CampaignStatisticsModel 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), @@ -269,7 +269,7 @@ class StatisticsScreen extends StatelessWidget { ); } - Future _loadStatistics() async { + Future _loadStatistics() async { var campaignSettings = GetIt.I().campaign; if (campaignSettings.recentStatistics != null && diff --git a/lib/features/campaigns/widgets/close_edit_widget.dart b/lib/features/campaigns/widgets/close_edit_widget.dart new file mode 100644 index 00000000..b1bcb0f8 --- /dev/null +++ b/lib/features/campaigns/widgets/close_edit_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:gruene_app/i18n/translations.g.dart'; + +class CloseEditWidget extends StatelessWidget { + final void Function()? onEdit; + final void Function() onClose; + + const CloseEditWidget({ + super.key, + this.onEdit, + required this.onClose, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Row( + children: [ + GestureDetector( + onTap: onClose, + child: Icon(Icons.close), + ), + _getSaveAction(theme), + ], + ); + } + + Widget _getSaveAction(ThemeData theme) { + if (onEdit == null) return SizedBox.shrink(); + return Expanded( + child: GestureDetector( + onTap: onEdit, + child: Text( + t.common.actions.edit, + textAlign: TextAlign.right, + style: theme.textTheme.bodyLarge, + ), + ), + ); + } +} diff --git a/lib/features/campaigns/widgets/map_container.dart b/lib/features/campaigns/widgets/map_container.dart index a18038c5..9cfcb5be 100644 --- a/lib/features/campaigns/widgets/map_container.dart +++ b/lib/features/campaigns/widgets/map_container.dart @@ -24,6 +24,7 @@ import 'package:gruene_app/features/campaigns/widgets/map_controller.dart'; import 'package:gruene_app/features/campaigns/widgets/map_controller_simplified.dart'; import 'package:gruene_app/i18n/translations.g.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:turf/turf.dart' as turf; typedef OnMapCreatedCallback = void Function(MapController controller); typedef AddPOIClickedCallback = void Function(LatLng location); @@ -96,6 +97,8 @@ class _MapContainerState extends State implements MapController, M bool _showAddMarker = true; + bool _isInFocusMode = false; + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -115,7 +118,7 @@ class _MapContainerState extends State implements MapController, M height: 0, width: 0, ); - if (popups.isEmpty && _showAddMarker) { + if (popups.isEmpty && _showAddMarker & !_isInFocusMode) { addMarker = Center( child: Container( padding: EdgeInsets.only( @@ -318,7 +321,15 @@ class _MapContainerState extends State implements MapController, M CampaignConstants.markerLayerName, const SymbolLayerProperties( iconImage: ['get', 'status_type'], - iconSize: 2, + iconSize: [ + Expressions.interpolate, + ['linear'], + [Expressions.zoom], + 11, + 1, + 16, + 2, + ], iconAllowOverlap: true, ), enableInteraction: false, @@ -328,6 +339,25 @@ class _MapContainerState extends State implements MapController, M ['has', 'point_count'], ], ); + + // add selected map layers + await _controller!.addGeoJsonSource( + '${CampaignConstants.markerSourceName}_selected', + MarkerItemHelper.transformListToGeoJson([]).toJson(), + ); + + await _controller!.addSymbolLayer( + '${CampaignConstants.markerSourceName}_selected', + '${CampaignConstants.markerLayerName}_selected', + const SymbolLayerProperties( + iconImage: ['get', 'status_type'], + iconSize: 3, + iconAllowOverlap: true, + ), + enableInteraction: false, + minzoom: minZoomMarkerItems, + ); + // init context layers re-directed to context screens widget.addMapLayersForContext!(_controller!); } @@ -415,6 +445,44 @@ class _MapContainerState extends State implements MapController, M ); } + @override + Future setFocusToMarkerItem(Map feature) async { + // removes the add_marker + setState(() { + _isInFocusMode = true; + }); + // align map to show feature in center area + final coord = MapHelper.extractLatLngFromFeature(feature); + await moveMapIfItemIsOnBorder(coord, Size(150, 150)); + // set opacity of marker layer + await _controller!.setLayerProperties( + CampaignConstants.markerLayerName, + SymbolLayerProperties(iconOpacity: 0.2), + ); + // set data for '_selected layer' + var featureObject = turf.Feature.fromJson(feature); + turf.FeatureCollection collection = turf.FeatureCollection(features: [featureObject]); + await _controller!.setGeoJsonSource( + '${CampaignConstants.markerSourceName}_selected', + collection.toJson(), + ); + } + + @override + Future unsetFocusToMarkerItem() async { + setState(() { + _isInFocusMode = false; + }); + await _controller!.setLayerProperties( + CampaignConstants.markerLayerName, + SymbolLayerProperties(iconOpacity: 1), + ); + await _controller!.setGeoJsonSource( + '${CampaignConstants.markerSourceName}_selected', + turf.FeatureCollection(features: []).toJson(), + ); + } + Future moveMapIfItemIsOnBorder(LatLng itemCoordinate, Size desiredSize) async { final mediaQuery = MediaQuery.of(context); final currentSize = mediaQuery.size; diff --git a/lib/features/campaigns/widgets/map_controller.dart b/lib/features/campaigns/widgets/map_controller.dart index 464245ed..72e505fc 100644 --- a/lib/features/campaigns/widgets/map_controller.dart +++ b/lib/features/campaigns/widgets/map_controller.dart @@ -43,4 +43,7 @@ abstract class MapController { void toggleInfoForMissingMapFeatures(bool enable); void navigateMapTo(LatLng location); + + Future setFocusToMarkerItem(Map feature); + Future unsetFocusToMarkerItem(); } diff --git a/lib/i18n/app_de.json b/lib/i18n/app_de.json index facd9b07..e00bcf1a 100644 --- a/lib/i18n/app_de.json +++ b/lib/i18n/app_de.json @@ -64,16 +64,24 @@ "info_poster_guidelines": "Wusstest Du, dass es je nach Region unterschiedliche gesetzliche Vorgaben und Regeln für das Aufhängen von Plakaten gibt? Bitte wende Dich an Deinen Orts- oder Kreisverband für weitere Informationen, bevor Du Plakate aufhängst!", "status": { "damaged": { - "label": "Beschädigt", - "hint": "Durch Fremdeinwirkung beschädigt" + "label": "Beschädigt (Als zu ersetzen melden)", + "description": "Plakat ist beschädigt, bitte ersetzen!" }, "missing": { "label": "Verschollen", - "hint": "Durch Fremdeinwirkung entfernt" + "description": "Plakat ist verschollen, bitte ersetzen!" }, "removed": { "label": "Abgehängt", - "hint": "Dauerhaft entfernt" + "description": "Plakat wurde abgehängt." + }, + "to_be_moved": { + "label": "Umzuhängen", + "description": "Plakat hängt falsch, bitte umhängen!" + }, + "ok": { + "label": "Plakat hängt", + "description": "Plakat hängt, kein Handlungsbedarf." } }, "comment": { diff --git a/pubspec.yaml b/pubspec.yaml index 643810d6..e2446dcb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,6 +94,7 @@ flutter: - assets/graphics/placeholders/ - assets/symbols/ - assets/symbols/posters/ + - assets/symbols/posters/svg_inverted/ - assets/symbols/doors/ - assets/symbols/flyer/ - assets/maps/ diff --git a/swaggers/gruene-api.yaml b/swaggers/gruene-api.yaml index 6c767d53..c1768980 100644 --- a/swaggers/gruene-api.yaml +++ b/swaggers/gruene-api.yaml @@ -331,7 +331,6 @@ paths: security: - api_key: [] - bearer: [] - - oauth2: [] post: operationId: createProfile summary: Create user profile @@ -356,7 +355,6 @@ paths: security: - api_key: [] - bearer: [] - - oauth2: [] /v1/profiles/self: get: operationId: getOwnProfile @@ -378,7 +376,6 @@ paths: security: - api_key: [] - bearer: [] - - oauth2: [] /v1/profiles/{profileId}: get: operationId: getProfile @@ -405,7 +402,6 @@ paths: security: - api_key: [] - bearer: [] - - oauth2: [] put: operationId: updateProfile summary: Update user profile @@ -442,7 +438,6 @@ paths: security: - api_key: [] - bearer: [] - - oauth2: [] delete: operationId: deleteProfile summary: Delete user profile @@ -468,7 +463,6 @@ paths: security: - api_key: [] - bearer: [] - - oauth2: [] /v1/profiles/{profileId}/image: put: operationId: updateProfileImage @@ -482,7 +476,8 @@ paths: - profileImage properties: profileImage: - type: file + type: string + format: binary parameters: - name: profileId required: true @@ -505,7 +500,6 @@ paths: security: - api_key: [] - bearer: [] - - oauth2: [] delete: operationId: deleteProfileImage summary: Delete user profile image @@ -531,7 +525,6 @@ paths: security: - api_key: [] - bearer: [] - - oauth2: [] /v1/profile-tags: get: operationId: findProfileTags @@ -918,7 +911,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] get: operationId: findAreas summary: Find Areas @@ -942,7 +934,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/areas/self: get: operationId: findOwnAreas @@ -967,7 +958,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/areas/{areaId}: get: operationId: getArea @@ -993,7 +983,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] put: operationId: updateArea summary: Update an Area @@ -1024,7 +1013,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] delete: operationId: deleteArea summary: Delete an Area @@ -1049,7 +1037,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/polling-stations: post: operationId: createPollingStation @@ -1205,7 +1192,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] get: operationId: findPois summary: Find POIs @@ -1240,7 +1226,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/pois/self: get: operationId: findOwnPois @@ -1276,7 +1261,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/pois/{poiId}: get: operationId: getPoi @@ -1302,7 +1286,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] put: operationId: updatePoi summary: Update a POI @@ -1333,7 +1316,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] delete: operationId: deletePoi summary: Delete a POI @@ -1358,7 +1340,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/pois/{poiId}/photos: post: operationId: addPoiPhoto @@ -1372,7 +1353,8 @@ paths: - image properties: image: - type: file + type: string + format: binary parameters: - name: poiId required: true @@ -1394,7 +1376,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/pois/{poiId}/photos/{photoId}: delete: operationId: deletePoiPhoto @@ -1425,7 +1406,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/routes: post: operationId: createRoute @@ -1592,7 +1572,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] get: operationId: findExperienceAreas summary: Find ExperienceAreas @@ -1616,7 +1595,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/experience-areas/{experienceAreaId}: get: operationId: getExperienceArea @@ -1642,7 +1620,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] put: operationId: updateExperienceArea summary: Update a ExperienceArea @@ -1673,7 +1650,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] delete: operationId: deleteExperienceArea summary: Delete a ExperienceArea @@ -1698,7 +1674,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/focus-areas: post: operationId: createFocusArea @@ -1723,7 +1698,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] get: operationId: findFocusAreas summary: Find FocusAreas @@ -1747,7 +1721,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/focus-areas/{focusAreaId}: get: operationId: getFocusArea @@ -1773,7 +1746,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] put: operationId: updateFocusArea summary: Update a FocusArea @@ -1804,7 +1776,6 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] delete: operationId: deleteFocusArea summary: Delete a FocusArea @@ -1829,11 +1800,10 @@ paths: - campaigns security: - bearer: [] - - oauth2: [] /v1/campaigns/statistics: get: - operationId: getStatistics - summary: Get statistics + operationId: getCampaignStatistics + summary: Get campaign statistics parameters: [] responses: '200': @@ -1841,7 +1811,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Statistics' + $ref: '#/components/schemas/CampaignStatistics' '401': description: '' tags: @@ -1899,7 +1869,6 @@ paths: - news security: - bearer: [] - - oauth2: [] /v1/news/{newsId}: get: operationId: getNews @@ -1925,7 +1894,31 @@ paths: - news security: - bearer: [] - - oauth2: [] + /v1/client-info: + get: + operationId: ClientInfoController_getClientInfo + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ClientInfo' + /v1/gnetz-applications: + get: + operationId: findGnetzApplications + summary: Find GNetz Applications + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/FindGnetzApplicationsResponse' + tags: + - gnetz-applications /health: get: tags: @@ -2427,14 +2420,17 @@ components: items: type: string purposes: - description: Purposes associated with email - examples: - - - privat - - - grüne - - - dienstlich - - - Postanschrift - - - Rechnungsanschrift - - - Lieferanschrift + description: |- + Purposes associated with email + Occuring values: + - privat + - grüne + - dienstlich + - Postanschrift + - Rechnungsanschrift + - Lieferanschrift + example: + - privat type: array items: type: string @@ -2623,6 +2619,19 @@ components: - tags - roles - achievements + OffsetPaginationMeta: + type: object + properties: + count: + type: number + total: + type: number + offset: + type: number + limit: + type: number + hasNext: + type: boolean FindProfilesResponse: type: object properties: @@ -2631,20 +2640,7 @@ components: items: $ref: '#/components/schemas/PublicProfile' meta: - type: object - properties: - count: - required: true - type: number - offset: - required: true - type: number - limit: - required: true - type: number - hasNext: - required: true - type: boolean + $ref: '#/components/schemas/OffsetPaginationMeta' required: - data - meta @@ -2871,20 +2867,7 @@ components: items: $ref: '#/components/schemas/ProfileTag' meta: - type: object - properties: - count: - required: true - type: number - total: - required: true - type: number - offset: - required: true - type: number - limit: - required: true - type: number + $ref: '#/components/schemas/OffsetPaginationMeta' required: - data - meta @@ -2906,6 +2889,11 @@ components: required: - id - username + KeysetPaginationMeta: + type: object + properties: + cursorNext: + type: string FindOffboardingUsersResponse: type: object properties: @@ -2914,11 +2902,7 @@ components: items: $ref: '#/components/schemas/OffboardingUserInfo' meta: - type: object - properties: - cursorNext: - required: false - type: string + $ref: '#/components/schemas/KeysetPaginationMeta' required: - data - meta @@ -2953,20 +2937,7 @@ components: items: $ref: '#/components/schemas/Division' meta: - type: object - properties: - count: - required: true - type: number - total: - required: true - type: number - offset: - required: true - type: number - limit: - required: true - type: number + $ref: '#/components/schemas/OffsetPaginationMeta' required: - data - meta @@ -3107,20 +3078,7 @@ components: items: $ref: '#/components/schemas/Role' meta: - type: object - properties: - count: - required: true - type: number - total: - required: true - type: number - offset: - required: true - type: number - limit: - required: true - type: number + $ref: '#/components/schemas/OffsetPaginationMeta' required: - data - meta @@ -3132,20 +3090,7 @@ components: items: $ref: '#/components/schemas/RoleTag' meta: - type: object - properties: - count: - required: true - type: number - total: - required: true - type: number - offset: - required: true - type: number - limit: - required: true - type: number + $ref: '#/components/schemas/OffsetPaginationMeta' required: - data - meta @@ -3157,20 +3102,7 @@ components: items: $ref: '#/components/schemas/RoleCategory' meta: - type: object - properties: - count: - required: true - type: number - total: - required: true - type: number - offset: - required: true - type: number - limit: - required: true - type: number + $ref: '#/components/schemas/OffsetPaginationMeta' required: - data - meta @@ -3200,27 +3132,26 @@ components: Polygon: type: object properties: - type: - type: string - description: Type of the polygon - example: Polygon - default: Polygon coordinates: type: array + description: |- + Coordinates of the polygon + Must follow the GeoJSON standard items: - name: coordinates type: array items: - required: true - description: |- - Coordinates of the polygon - Must follow the GeoJSON standard type: array items: type: number + format: double + type: + type: string + description: Type of the polygon + example: Polygon + default: Polygon required: - - type - coordinates + - type UpdateArea: type: object properties: @@ -3424,6 +3355,7 @@ components: - DAMAGED - REMOVED - MISSING + - TO_BE_MOVED type: string comment: type: string @@ -3851,7 +3783,7 @@ components: - division - state - germany - Statistics: + CampaignStatistics: type: object properties: poster: @@ -3932,6 +3864,122 @@ components: $ref: '#/components/schemas/News' required: - data + ClientInfo: + type: object + properties: + clientIp: + type: string + clientIpVersion: + enum: + - ivp4 + - ipv6 + type: string + required: + - clientIp + - clientIpVersion + GnetzApplicationCategory: + type: object + properties: + slug: + type: string + description: Category slug + example: beteiligung + title: + type: string + description: Category title + example: Beteiligung + order: + type: number + description: The category order used for display purposes + example: 1 + required: + - slug + - title + - order + GnetzApplication: + type: object + properties: + shortDescription: + type: object + additionalProperties: + type: string + example: + de_DE: Beschreibung in Deutsch + en_US: description in english + description: + type: object + additionalProperties: + type: string + example: + de_DE: Beschreibung in Deutsch + en_US: description in english + slug: + type: string + description: Application slug + example: wolke + url: + type: string + nullable: true + description: URL to the application + example: https://wolke.netzbegruenung.de + title: + type: string + description: Application title + example: Wolke + documentation: + type: string + nullable: true + description: URL to the documentation + example: https://netz.gruene.de/de/wissenswerk/2024-08/wolke + googleStore: + type: string + nullable: true + description: URL to google play store + example: https://play.google.com/store/apps/details?id=com.nextcloud.client + appleStore: + type: string + nullable: true + description: URL to apple store + example: https://apps.apple.com/us/app/nextcloud/id1125420102 + supportMail: + type: string + nullable: true + description: support email + example: support@example.com + icon: + type: string + nullable: true + description: icon as svg + example: >- + + categories: + description: Application categories + type: array + items: + $ref: '#/components/schemas/GnetzApplicationCategory' + required: + - shortDescription + - description + - slug + - url + - title + - documentation + - googleStore + - appleStore + - supportMail + - icon + - categories + FindGnetzApplicationsResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/GnetzApplication' + required: + - data HealthCheckResponse: type: object properties: