Add roles garde fou + update gradle + wip sectionAgenda (online and manual) + update EventConfig + missing fields in GuidedSteps update popup + updates in stats screen

This commit is contained in:
Thomas Fransolet 2026-03-25 17:41:32 +01:00
parent b7bd8588fe
commit f90f014f36
39 changed files with 2400 additions and 188 deletions

65
CLAUDE.md Normal file
View File

@ -0,0 +1,65 @@
# manager-app
Interface de gestion de contenu Flutter (web/desktop) pour configurer `tablet-app` et `mymuseum-visitapp`.
## Stack
- Flutter web (cible principale), support desktop partiel
- State management : **Provider** + `ChangeNotifier`
- Navigation : **GoRouter**
- Firebase Storage (upload de ressources)
## Structure
```
lib/
├── main.dart # Entry point, init Firebase, GoRouter, breakpoints responsive
├── app_context.dart # Provider root wrapping ManagerAppContext
├── client.dart # Wrapper exposant les 12+ facades API générées
├── constants.dart # Couleurs, types de sections, langues, types de ressources
├── Models/ # ManagerAppContext (état central), Session, Menu...
├── Helpers/ # FileHelper (localStorage session), PDFHelper
├── Components/ # 48+ widgets réutilisables (inputs, pickers, players...)
└── Screens/ # Écrans par domaine
├── Main/ # Dashboard principal avec menu latéral
├── Configurations/ # Builder de configuration par type de section
│ └── SubSection/ # Map, Menu, Slider, Quiz, Article, PDF, Video, Weather, Event, Parcours, Game, Agenda
├── Resources/ # CRUD ressources (Image, Video, Audio, PDF, JSON...)
├── Applications/ # Liaison config ↔ app device
├── Kiosk_devices/ # Gestion des devices
├── Users/ # Gestion des utilisateurs
├── ApiKeys/ # Gestion des clés API
├── Notifications/ # Push notifications
└── Statistics/ # Analytics visiteurs
```
## Client API généré (manager_api_new/)
**C'est ici que le client est généré.** Il est consommé via dépendance locale par `tablet-app` et `mymuseum-visitapp`.
- **Ne pas modifier manuellement** les fichiers dans `manager_api_new/`
- Régénérer via OpenAPI generator quand le backend change : `openapi_generator_config.json` à la racine
- 16 classes API : AuthenticationApi, ConfigurationApi, SectionApi, SectionMapApi, SectionEventApi, ResourceApi, DeviceApi, UserApi, StatsApi, AIApi, etc.
- 130+ DTOs générés dans `manager_api_new/lib/model/`
## État central (ManagerAppContext)
Contient : credentials, accessToken, instanceId, instanceDTO, référence au client API, configuration/section sélectionnée.
Accès via `context.read<AppContext>()` ou `context.watch<AppContext>()`.
## Flow d'authentification
1. `FileHelper().readSessionWeb()` → localStorage
2. Si pas de session → `/login`
3. POST `/api/Authentication/AuthenticateWithJson` → TokenDTO
4. Init `Client` avec le host backend
5. Fetch `InstanceDTO` → détermine les features (isMobile, isTablet, isWeb, isVR, isStatistic)
6. Redirect vers `/main/:view`
## Breakpoints responsive
- Mobile : 0550px
- Tablet : 550800px
- Desktop : 8011920px
- 4K : 1921px+
## Commandes utiles
```bash
flutter run -d chrome # Lancer en web
flutter build web # Build web
flutter pub run build_runner build # Regénérer code généré
```

View File

@ -1,12 +1,12 @@
buildscript { buildscript {
ext.kotlin_version = '1.7.10' ext.kotlin_version = '1.9.0'
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.1' classpath 'com.android.tools.build:gradle:8.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }
@ -14,7 +14,7 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
} }

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

3
devtools_options.yaml Normal file
View File

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@ -1,57 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:manager_app/Components/loader_animated_pieces.dart';
import 'package:manager_app/constants.dart';
class CommonLoader extends StatefulWidget { class CommonLoader extends StatelessWidget {
double? iconSize; final double? iconSize;
CommonLoader({Key? key, this.iconSize}) : super(key: key); const CommonLoader({Key? key, this.iconSize}) : super(key: key);
@override
State<CommonLoader> createState() => _CommonLoaderState();
}
class _CommonLoaderState extends State<CommonLoader> with TickerProviderStateMixin {
AnimationController? _controller;
@override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 5000),
vsync: this,
)..repeat();
super.initState();
}
@override
void dispose() {
_controller!.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size; final size = iconSize ?? 60;
_controller!.forward(from: 0.0);
_controller!.addListener(() {
if (_controller!.isCompleted) {
_controller!.reverse();
}
if(_controller!.isDismissed){
_controller!.forward();
}
});
return Center( return Center(
child: RotationTransition( child: LoaderWaveFloat(size: size),
turns: Tween(begin: 0.0, end: 3.0).animate(_controller!),
child: SizedBox(
height: 45,
child: SvgPicture.asset('assets/images/MyInfoMate_logo_only.svg')
)/*Icon(Icons.museum_outlined, color: kPrimaryColor, size: widget.iconSize == null ? size.height*0.1 : widget.iconSize!)*/,
),
); );
} }
} }

View File

@ -0,0 +1,130 @@
import 'dart:math' as dart_math;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
String _makeSvg(String d, String fill, String transform) =>
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 439 419">'
'<path d="$d" fill="$fill" transform="$transform"/>'
'</svg>';
final List<String> _svgStrings = [
// path1 pink/red
_makeSvg(
'm 178.04447,135.21247 c 0.002,9.09742 0.17625,49.4843 0.17178,58.58172 -0.005,10.44131 -0.004,20.8826 0.002,31.32392 0.005,9.06176 0.005,18.12351 0.003,27.18527 -0.002,5.37036 -0.002,10.7407 0.002,16.11105 0.0204,38.14483 0.0204,38.14483 -0.54694,53.99548 l -0.10305,3.01772 c -0.80885,18.6575 -7.8096,35.94603 -20.4863,49.70518 l -2.15235,2.35938 c -12.39998,12.98838 -30.69723,23.86033 -49.02594,24.74586 -2.64076,0.0575 -5.28022,0.0839 -7.921566,0.10008 l -3.044189,0.03 c -3.328264,0.0316 -6.656542,0.0564 -9.984864,0.0811 -2.346596,0.0206 -4.693189,0.0416 -7.039779,0.063 -4.940416,0.044 -9.880843,0.0849 -14.821305,0.12331 -6.216775,0.0484 -12.433489,0.10212 -18.650202,0.158 -6.037887,0.054 -17.776846,0.19345 -40.2740759,0.28883 L 57.571753,137.10368 c 14.685204,-1.44088 35.90966,0.0974 45.537897,0.12284 41.79401,0.11064 74.93393,-6.04872 74.93482,-2.01405 z',
'#d62f60',
'translate(249.91406,6.8671875)',
),
// path2 teal
_makeSvg(
'm 64.688834,203.13197 c 0.0035,6.66536 0.0067,13.33073 0.0093,19.99609 l 9.86e-4,2.48865 c 0.005,13.72121 -0.0058,27.44236 -0.02756,41.16355 -0.01265,8.44313 -0.01133,16.88604 0.0085,25.32916 0.01266,5.78741 0.0111,11.57471 -0.0014,17.36211 -0.0067,3.33159 -0.0047,6.66256 0.01058,9.99416 0.111194,25.65838 0.111194,25.65838 -4.500365,37.04128 l -0.849251,2.18899 c -7.771476,18.55174 -21.788527,30.7335 -39.685905,38.97898 -3.37688,1.34444 -5.301227,2.48389 -8.978708,2.48389 l -3.2442484,1.47057 c -4.8987376,0.0599 -14.0045433,0.089 -15.4540455,0.10655 -25.7222011,0.30356 -49.7229121,-5.03616 -68.8916011,-23.19214 -3.286346,-3.24956 -6.197141,-6.66938 -8.810303,-10.47827 -1.170385,-1.6798 -3.844026,-5.57044 -5.590218,-8.45711 -0.851523,-1.40767 -3.595298,-4.4516 -7.983006,-16.21404 -0.853709,-1.50963 -2.403339,-12.94213 -2.403339,-12.94213 -0.0915,-1.48718 -0.23669,-2.92345 -0.23801,-4.41344 l -0.01,-2.78866 0.005,-3.06828 -0.007,-3.25582 c -0.006,-3.61823 -0.006,-7.23642 -0.005,-10.85466 -0.003,-2.58667 -0.006,-5.17335 -0.0102,-7.76002 -0.008,-6.30723 -0.0111,-12.61445 -0.0116,-18.92169 -4.7e-4,-5.12481 -0.003,-10.24962 -0.006,-15.37442 -0.009,-14.52085 -0.0134,-29.0417 -0.0127,-43.56255 l 1.3e-4,-2.37302 1.2e-4,-2.3759 c 4.1e-4,-12.71422 -0.009,-25.42841 -0.0233,-38.14262 -0.0143,-13.04611 -0.0213,-26.0922 -0.0204,-39.13831 3.3e-4,-7.32798 -0.002,-14.65593 -0.0132,-21.9839 -0.01,-6.88802 -0.01,-13.77597 -0.002,-20.663998 10e-4,-2.533683 1.124,-6.567371 1.118,-9.101047 -0.008,-3.44738 -1.128,-5.394528 82.246422,49.699935 83.374422,55.09446 83.377322,60.92629 83.380322,66.75811 z',
'#51cdc8',
'translate(112.81113,8.4930303)',
),
// path3 yellow
_makeSvg(
'm 0,0 c 2.3047995,-6.7559e-4 4.6095989,-0.00164773 6.9143982,-0.00289917 4.8125578,-0.00146675 9.6250968,6.4331e-4 14.4376528,0.00534058 6.108101,0.00567724 12.216159,0.00242443 18.324259,-0.00357247 4.756666,-0.00364996 9.513323,-0.00244655 14.269991,1.2779e-4 2.249005,6.5808e-4 4.498012,-1.1905e-4 6.747016,-0.00247955 10.980782,-0.00899429 21.945816,0.06768592 32.917523,0.54693985 l 2.536386,0.10305596 C 119.97909,1.8334817 140.8053,14.193522 157.08594,31.132812 c 14.55199,16.47268 20.55085,36.163532 20.75,57.8125 0.0272,1.535344 0.0557,3.070663 0.0855,4.605958 0.0684,3.811023 0.11277,7.62181 0.14563,11.43329 0.018,2.0392 0.0412,21.15431 0.0616,30.38266 0.0376,3.46864 -119.042723,1.76559 -119.042723,1.76559 l -20.85965,1.44008 -111.140349,85.55992 c -0.161213,-39.22828 -0.161213,-39.22828 -0.195313,-55.73828 -0.02368,-11.37426 -0.05144,-22.7484 -0.106445,-34.12256 -0.04005,-8.28583 -0.06572,-16.57157 -0.0746,-24.8575 -0.0052,-4.3814 -0.01724,-8.76252 -0.04657,-13.143825 -0.02746,-4.136862 -0.03546,-8.273336 -0.02951,-12.41028 -0.0011,-1.50588 -0.0089,-3.011774 -0.02439,-4.517574 -0.131739,-13.445652 1.806738,-26.659985 8.289322,-38.647479 l 1.049072,-2.008056 c 2.851223,-5.317287 6.068313,-10.084275 10.138428,-14.554444 L -51.898437,21.875 C -43.655062,13.127533 -37.90534,5.8098811 -26.601562,1.8203125 l -12.3125,0.3125 v -1 C -25.942811,-0.00501656 -13.012554,-0.01357216 0,0 Z',
'#fec728',
'translate(249.91406,6.8671875)',
),
// path4 dark blue
_makeSvg(
'M 122.4545,-1.5555303 C 104.75322,5.7344897 77.764999,26.892615 70.396697,44.304966 65.927561,56.152831 64.595666,67.366945 64.64297,79.919079 c -0.0088,1.568853 -0.01926,3.137697 -0.03133,4.706528 -0.02754,4.203537 -0.03127,8.406777 -0.03028,12.610391 -0.0032,4.413592 -0.02883,8.827052 -0.05198,13.240572 -0.04035,8.33285 -0.06085,16.66561 -0.07397,24.99854 -0.01594,9.49723 -0.05439,18.99434 -0.09459,28.4915 -0.08212,19.51341 -0.136384,39.02681 -0.171948,58.54036 -3.137967,0.048 -6.275837,0.0763 -9.414062,0.10156 l -2.652588,0.0413 C 23.81406,222.81977 1.5565346,207.45478 -18.375829,188.75648 l -2.718735,-2.53775 c -8.235245,-7.71396 -16.371582,-15.53064 -24.472001,-23.38588 -5.420898,-5.25288 -10.871421,-10.46504 -16.411803,-15.59199 -41.322082,-38.29606 -41.811362,-38.47284 -39.697042,-61.82222 2.114319,-23.349383 3.306846,-25.445419 3.989278,-27.72417 6.748595,-20.960043 21.800751,-37.772405 41,-48.4375003 18.641419,-9.40752742 36.480818,-9.88154862 87.01386,-9.85750697 50.533043,0.0240417 73.811792,-1.09807633 92.064262,-0.98624303',
'#264863',
'translate(112.81113,8.4930303)',
),
// path5 purple
_makeSvg(
'm 0,0 131.25,-0.25 v 63.875 l 0.11035,19.705566 c 0.0181,7.998291 0.0181,7.998291 0.0214,11.753754 0.006,2.598872 0.0186,5.19694 0.0405,7.79565 0.033,3.95755 0.0333,7.91478 0.0318,11.87247 l 0.0436,3.45275 c -0.0875,17.24733 -6.73078,32.78677 -18.61099,45.21278 C 106.22208,169.37917 98.630621,174.4762 90,177 l -11.397775,1.80377 -1.45256,0.16395 c -7.104685,-0.0441 -3.43355,-0.0556 -10.538337,-0.0776 -2.653987,-0.0101 -5.307964,-0.0237 -7.961914,-0.041 -3.807645,-0.0242 -7.615144,-0.0356 -11.422852,-0.0444 l -3.614349,-0.0313 -3.336334,-4.6e-4 -2.947357,-0.0135 c -2.521553,0.0131 41.626196,-2.01422 39.819227,0.21323 L 33,179 H 30.1875 24 c -1.532555,0.0113 -3.065107,0.0231 -4.597656,0.0352 -1.821614,0.009 -3.643229,0.0184 -5.464844,0.0273 l -2.798828,0.0254 -2.6738282,0.01 -2.4494629,0.0159 C 4,179 5.0139855,178.84803 4.0508683,177.7768 l 1.9580347,1.35049 -19.547353,-0.15833 -0.795115,0.0476 L -16,179 l -3.272949,0.0317 c -3.999316,0.0361 -7.998616,0.0591 -11.998047,0.0781 -1.732447,0.0101 -3.464876,0.0237 -5.197266,0.041 -2.487184,0.0242 -4.974145,0.0356 -7.461425,0.0444 l -2.35495,0.0313 c -1.906618,4e-4 -13.996389,0.0497 -15.899078,-0.0727 l 18.898307,-4.96534 C -32.858078,170.63158 -17.423649,159.0936 -8,141 c 5.0244847,-11.01947 7.29498468,-20.88967 7.31884766,-32.92383 l 0.0307674,-3.60735 c 0.0305076,-3.83335 0.0477557,-7.666634 0.0644474,-11.50007 0.0169446,-2.374681 0.0347624,-4.749356 0.0534668,-7.124023 C -0.4694063,77.063221 -0.4232179,68.2816 -0.375,59.5 Z',
'#773aaa',
'translate(177,231)',
),
// path6 orange
_makeSvg(
'm 130.75,72.125 c 0,0 -130.9459176,38.46063 -130.5354588,-0.05093 L 130.75,72.125 l 0.5,86.75 L 0,159 0.2145412,72.074065',
'#ef7a34',
'translate(177,72)',
),
];
// LoaderWaveFloat
// Wave opacity sweeping through pieces + slow rotation + vertical float.
class LoaderWaveFloat extends StatefulWidget {
final double size;
const LoaderWaveFloat({Key? key, required this.size}) : super(key: key);
@override
State<LoaderWaveFloat> createState() => _LoaderWaveFloatState();
}
class _LoaderWaveFloatState extends State<LoaderWaveFloat>
with TickerProviderStateMixin {
late final AnimationController _wave;
late final AnimationController _rotate;
late final AnimationController _float;
late final Animation<double> _floatAnim;
static const _opacityMin = 0.82;
static const _opacityMax = 0.97;
static const List<double> _phases = [0.0, 1 / 6, 2 / 6, 3 / 6, 4 / 6, 5 / 6];
@override
void initState() {
super.initState();
_wave = AnimationController(vsync: this, duration: const Duration(milliseconds: 4000))
..repeat();
_rotate = AnimationController(vsync: this, duration: const Duration(milliseconds: 6000))
..repeat();
_float = AnimationController(vsync: this, duration: const Duration(milliseconds: 1800))
..repeat(reverse: true);
_floatAnim = Tween<double>(begin: -4.0, end: 4.0)
.animate(CurvedAnimation(parent: _float, curve: Curves.easeInOut));
}
@override
void dispose() {
_wave.dispose();
_rotate.dispose();
_float.dispose();
super.dispose();
}
double _opacity(int i) {
final t = _wave.value;
final phase = _phases[i];
final v = 0.5 - 0.5 * dart_math.cos(2 * dart_math.pi * ((t - phase) % 1.0));
return _opacityMin + (_opacityMax - _opacityMin) * v;
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge([_wave, _rotate, _float]),
builder: (_, __) {
return Transform.translate(
offset: Offset(0, _floatAnim.value),
child: Transform.rotate(
angle: _rotate.value * 2 * dart_math.pi,
child: SizedBox(
width: widget.size,
height: widget.size,
child: Stack(
children: List.generate(
6,
(i) => Opacity(
opacity: _opacity(i),
child: SvgPicture.string(
_svgStrings[i],
width: widget.size,
height: widget.size,
),
),
),
),
),
),
);
},
);
}
}

View File

@ -17,6 +17,8 @@ class ManagerAppContext with ChangeNotifier{
bool? isLoading = false; bool? isLoading = false;
UserRole? role; UserRole? role;
bool get canEdit => role != null && role!.value <= 2;
ManagerAppContext({this.email, this.accessToken}); ManagerAppContext({this.email, this.accessToken});
// Implement toString to make it easier to see information about // Implement toString to make it easier to see information about

View File

@ -19,8 +19,9 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
List<Map<String, dynamic>> _keys = []; List<Map<String, dynamic>> _keys = [];
bool _loading = true; bool _loading = true;
static String _appTypeName(int? v) { static String _appTypeName(dynamic v) {
switch (v) { if (v is String) return v;
switch (v as int?) {
case 0: return 'VisitApp'; case 0: return 'VisitApp';
case 1: return 'TabletApp'; case 1: return 'TabletApp';
default: return 'Other'; default: return 'Other';
@ -42,9 +43,9 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
} }
} }
Future<String?> _createKey(ManagerAppContext ctx, String name, ApiKeyAppType appType) async { Future<String?> _createKey(ManagerAppContext ctx, String name, ApiKeyAppType appType, DateTime? dateExpiration) async {
final response = await ctx.clientAPI!.apiKeyApi!.apiKeyCreateApiKeyWithHttpInfo( final response = await ctx.clientAPI!.apiKeyApi!.apiKeyCreateApiKeyWithHttpInfo(
CreateApiKeyRequest(name: name, appType: appType), CreateApiKeyRequest(name: name, appType: appType, dateExpiration: dateExpiration),
); );
if (response.statusCode == 200 || response.statusCode == 201) { if (response.statusCode == 200 || response.statusCode == 201) {
final Map<String, dynamic> json = jsonDecode(utf8.decode(response.bodyBytes)); final Map<String, dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
@ -62,6 +63,7 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) { void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
final nameCtrl = TextEditingController(); final nameCtrl = TextEditingController();
ApiKeyAppType selectedType = ApiKeyAppType.number0; ApiKeyAppType selectedType = ApiKeyAppType.number0;
DateTime? selectedExpiration;
showDialog( showDialog(
context: context, context: context,
@ -85,13 +87,44 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
), ),
], ],
), ),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
selectedExpiration == null
? 'Pas d\'expiration'
: 'Expire le ${selectedExpiration!.day.toString().padLeft(2, '0')}/${selectedExpiration!.month.toString().padLeft(2, '0')}/${selectedExpiration!.year}',
style: const TextStyle(fontSize: 14),
),
),
TextButton(
onPressed: () async {
final picked = await showDatePicker(
context: ctx2,
initialDate: DateTime.now().add(const Duration(days: 365)),
firstDate: DateTime.now().add(const Duration(days: 1)),
lastDate: DateTime.now().add(const Duration(days: 365 * 5)),
);
if (picked != null) setLocal(() => selectedExpiration = picked);
},
child: const Text('Choisir'),
),
if (selectedExpiration != null)
IconButton(
icon: const Icon(Icons.close, size: 16),
onPressed: () => setLocal(() => selectedExpiration = null),
tooltip: 'Supprimer l\'expiration',
),
],
),
]), ]),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx2), child: const Text('Annuler')), TextButton(onPressed: () => Navigator.of(ctx2, rootNavigator: true).pop(), child: const Text('Annuler')),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
Navigator.pop(ctx2); Navigator.of(ctx2, rootNavigator: true).pop();
final plainKey = await _createKey(ctx, nameCtrl.text, selectedType); final plainKey = await _createKey(ctx, nameCtrl.text, selectedType, selectedExpiration);
if (plainKey != null && context.mounted) { if (plainKey != null && context.mounted) {
_showPlainKeyDialog(context, plainKey); _showPlainKeyDialog(context, plainKey);
} }
@ -135,7 +168,7 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
]), ]),
actions: [ actions: [
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.of(context, rootNavigator: true).pop(),
child: const Text('J\'ai copié la clé'), child: const Text('J\'ai copié la clé'),
), ),
], ],
@ -146,15 +179,15 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
void _confirmRevoke(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> key) { void _confirmRevoke(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> key) {
showDialog( showDialog(
context: context, context: context,
builder: (_) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: const Text('Révoquer la clé API'), title: const Text('Révoquer la clé API'),
content: Text('Révoquer « ${key['name']} » ? Les apps utilisant cette clé perdront l\'accès.'), content: Text('Révoquer « ${key['name']} » ? Les apps utilisant cette clé perdront l\'accès.'),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Annuler')), TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Annuler')),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red), style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(dialogContext);
await _revokeKey(ctx, key['id'] as String); await _revokeKey(ctx, key['id'] as String);
}, },
child: const Text('Révoquer', style: TextStyle(color: Colors.white)), child: const Text('Révoquer', style: TextStyle(color: Colors.white)),
@ -199,38 +232,64 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
const Center(child: Text('Aucune clé API')) const Center(child: Text('Aucune clé API'))
else else
Expanded( Expanded(
child: Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.grey.shade200),
),
clipBehavior: Clip.antiAlias,
child: SingleChildScrollView( child: SingleChildScrollView(
child: SizedBox(
width: double.infinity,
child: DataTable( child: DataTable(
horizontalMargin: 16,
columnSpacing: 24,
headingRowColor: WidgetStateProperty.all(Colors.grey.shade50),
dividerThickness: 1,
columns: const [ columns: const [
DataColumn(label: Text('Nom')), DataColumn(label: Text('Nom', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Type')), DataColumn(label: Text('Type', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Créée le')), DataColumn(label: Text('Créée le', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Statut')), DataColumn(label: Text('Expiration', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Actions')), DataColumn(label: Text('Statut', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Actions', style: TextStyle(fontWeight: FontWeight.w600))),
], ],
rows: _keys.map((key) { rows: _keys.map((key) {
final isActive = key['isActive'] as bool? ?? false; final isActive = key['isActive'] as bool? ?? false;
final dateRaw = key['dateCreation'] as String?; final dateRaw = key['dateCreation'] as String?;
final date = dateRaw != null final date = dateRaw != null ? DateTime.tryParse(dateRaw)?.toLocal() : null;
? DateTime.tryParse(dateRaw)?.toLocal()
: null;
final dateStr = date != null final dateStr = date != null
? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}' ? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'
: ''; : '';
final expRaw = key['dateExpiration'] as String?;
final exp = expRaw != null ? DateTime.tryParse(expRaw)?.toLocal() : null;
final expStr = exp != null
? '${exp.day.toString().padLeft(2, '0')}/${exp.month.toString().padLeft(2, '0')}/${exp.year}'
: '';
return DataRow(cells: [ return DataRow(cells: [
DataCell(Text(key['name'] as String? ?? '')), DataCell(Text(key['name'] as String? ?? '')),
DataCell(Text(_appTypeName(key['appType'] as int?))), DataCell(Text(_appTypeName(key['appType']))),
DataCell(Text(dateStr)), DataCell(Text(dateStr, style: const TextStyle(color: Colors.grey))),
DataCell(Text(expStr, style: TextStyle(color: exp != null && exp.isBefore(DateTime.now()) ? Colors.red : Colors.grey))),
DataCell(Chip( DataCell(Chip(
label: Text(isActive ? 'Active' : 'Révoquée', label: Text(
style: TextStyle(color: isActive ? Colors.green.shade700 : Colors.red.shade700, fontSize: 12)), isActive ? 'Active' : 'Révoquée',
style: TextStyle(
color: isActive ? Colors.green.shade700 : Colors.red.shade700,
fontSize: 12,
),
),
backgroundColor: isActive ? Colors.green.shade50 : Colors.red.shade50, backgroundColor: isActive ? Colors.green.shade50 : Colors.red.shade50,
side: BorderSide(color: isActive ? Colors.green.shade200 : Colors.red.shade200), side: BorderSide(color: isActive ? Colors.green.shade200 : Colors.red.shade200),
padding: const EdgeInsets.symmetric(horizontal: 4),
visualDensity: VisualDensity.compact,
)), )),
DataCell(isActive DataCell(isActive
? IconButton( ? IconButton(
icon: const Icon(Icons.block, color: Colors.red), icon: const Icon(Icons.block, color: Colors.red, size: 20),
tooltip: 'Révoquer', tooltip: 'Révoquer',
onPressed: () => _confirmRevoke(context, managerCtx, key), onPressed: () => _confirmRevoke(context, managerCtx, key),
) )
@ -240,6 +299,8 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
), ),
), ),
), ),
),
),
], ],
); );
} }

View File

@ -86,7 +86,7 @@ class _AgendaConfigState extends State<AgendaConfig> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildAgendaHeader(size, mapProviderIn), _buildAgendaHeader(size, mapProviderIn),
if (agendaDTO.isOnlineAgenda == false) ...[ ...[
const Divider(height: 32), const Divider(height: 32),
Padding( Padding(
padding: padding:
@ -94,7 +94,7 @@ class _AgendaConfigState extends State<AgendaConfig> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text("Évènements Manuels", const Text("Évènements",
style: style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
ElevatedButton.icon( ElevatedButton.icon(
@ -146,7 +146,7 @@ class _AgendaConfigState extends State<AgendaConfig> {
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: events.isEmpty child: events.isEmpty
? const Center( ? const Center(
child: Text("Aucun évènement manuel", child: Text("Aucun évènement",
style: TextStyle(fontStyle: FontStyle.italic))) style: TextStyle(fontStyle: FontStyle.italic)))
: ListView.builder( : ListView.builder(
itemCount: events.length, itemCount: events.length,
@ -288,6 +288,21 @@ class _AgendaConfigState extends State<AgendaConfig> {
}); });
}, },
), ),
CheckInputContainer(
label: "Vue carte :",
isChecked: agendaDTO.agendaMapProvider != null,
onChanged: (value) {
setState(() {
if (value) {
agendaDTO.agendaMapProvider = MapProvider.Google;
} else {
agendaDTO.agendaMapProvider = null;
}
widget.onChanged(agendaDTO);
});
},
),
if (agendaDTO.agendaMapProvider != null)
SingleSelectContainer( SingleSelectContainer(
label: "Service carte :", label: "Service carte :",
color: Colors.black, color: Colors.black,

View File

@ -6,6 +6,7 @@ import 'package:manager_app/Components/rounded_button.dart';
import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart';
import 'package:manager_app/Components/string_input_container.dart'; import 'package:manager_app/Components/string_input_container.dart';
import 'package:manager_app/Components/geometry_input_container.dart'; import 'package:manager_app/Components/geometry_input_container.dart';
import 'package:manager_app/Components/resource_input_container.dart';
void showNewOrUpdateEventAgenda( void showNewOrUpdateEventAgenda(
BuildContext context, BuildContext context,
@ -95,6 +96,117 @@ void showNewOrUpdateEventAgenda(
], ],
), ),
Divider(height: 24), Divider(height: 24),
// Dates
Row(
children: [
SizedBox(
width: halfWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Date de début :"),
SizedBox(height: 4),
OutlinedButton.icon(
icon: Icon(Icons.calendar_today, size: 16),
label: Text(workingEvent.dateFrom != null
? "${workingEvent.dateFrom!.day.toString().padLeft(2, '0')}/${workingEvent.dateFrom!.month.toString().padLeft(2, '0')}/${workingEvent.dateFrom!.year}"
: "Choisir une date"),
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: workingEvent.dateFrom ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) setState(() => workingEvent.dateFrom = picked);
},
),
],
),
),
SizedBox(width: 20),
SizedBox(
width: halfWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Date de fin :"),
SizedBox(height: 4),
OutlinedButton.icon(
icon: Icon(Icons.calendar_today, size: 16),
label: Text(workingEvent.dateTo != null
? "${workingEvent.dateTo!.day.toString().padLeft(2, '0')}/${workingEvent.dateTo!.month.toString().padLeft(2, '0')}/${workingEvent.dateTo!.year}"
: "Choisir une date"),
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: workingEvent.dateTo ?? workingEvent.dateFrom ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) setState(() => workingEvent.dateTo = picked);
},
),
],
),
),
],
),
Divider(height: 24),
// Ressources
Row(
children: [
SizedBox(
width: halfWidth,
child: ResourceInputContainer(
label: "Image :",
initialValue: workingEvent.resourceId,
inResourceTypes: const [ResourceType.Image, ResourceType.ImageUrl],
onChanged: (res) => setState(() {
workingEvent.resourceId = res.id;
workingEvent.resource = EventAgendaDTOResource.fromJson(res.toJson());
}),
),
),
SizedBox(width: 20),
SizedBox(
width: halfWidth,
child: ResourceInputContainer(
label: "Vidéo :",
initialValue: workingEvent.videoResourceId,
inResourceTypes: const [ResourceType.Video, ResourceType.VideoUrl],
onChanged: (res) => setState(() {
workingEvent.videoResourceId = res.id;
workingEvent.videoResource = EventAgendaDTOResource.fromJson(res.toJson());
}),
),
),
],
),
Divider(height: 24),
// Vidéo externe
Row(
children: [
SizedBox(
width: halfWidth,
child: StringInputContainer(
label: "ID YouTube :",
initialValue: workingEvent.idVideoYoutube ?? "",
onChanged: (val) => setState(() => workingEvent.idVideoYoutube = val.isEmpty ? null : val),
),
),
SizedBox(width: 20),
SizedBox(
width: halfWidth,
child: StringInputContainer(
label: "Lien vidéo direct :",
initialValue: workingEvent.videoLink ?? "",
onChanged: (val) => setState(() => workingEvent.videoLink = val.isEmpty ? null : val),
),
),
],
),
Divider(height: 24),
// Site | Tel | Email // Site | Tel | Email
Row( Row(
children: [ children: [

View File

@ -8,6 +8,9 @@ import 'package:provider/provider.dart';
import 'package:manager_app/app_context.dart'; import 'package:manager_app/app_context.dart';
import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/Components/message_notification.dart'; import 'package:manager_app/Components/message_notification.dart';
import 'package:manager_app/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart';
import 'package:manager_app/Components/dropDown_input_container.dart';
import 'showNewOrUpdateMapAnnotation.dart';
class EventConfig extends StatefulWidget { class EventConfig extends StatefulWidget {
final SectionEventDTO initialValue; final SectionEventDTO initialValue;
@ -25,12 +28,17 @@ class EventConfig extends StatefulWidget {
class _EventConfigState extends State<EventConfig> { class _EventConfigState extends State<EventConfig> {
late SectionEventDTO eventDTO; late SectionEventDTO eventDTO;
List<SectionDTO> availableMaps = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
eventDTO = widget.initialValue; eventDTO = widget.initialValue;
WidgetsBinding.instance.addPostFrameCallback((_) => _loadProgrammeBlocks()); WidgetsBinding.instance.addPostFrameCallback((_) {
_loadProgrammeBlocks();
_loadGlobalAnnotations();
_loadAvailableMaps();
});
} }
Future<void> _loadProgrammeBlocks() async { Future<void> _loadProgrammeBlocks() async {
@ -48,6 +56,36 @@ class _EventConfigState extends State<EventConfig> {
} }
} }
Future<void> _loadGlobalAnnotations() async {
if (eventDTO.id == null || !mounted) return;
final appContext = Provider.of<AppContext>(context, listen: false);
final api = (appContext.getContext() as ManagerAppContext).clientAPI!.sectionEventApi!;
try {
final annotations = await api.sectionEventGetGlobalMapAnnotations(eventDTO.id!);
if (annotations == null || !mounted) return;
setState(() {
eventDTO.globalMapAnnotations = annotations;
});
} catch (e) {
// Silently keep initial value on error
}
}
Future<void> _loadAvailableMaps() async {
if (eventDTO.configurationId == null || !mounted) return;
final appContext = Provider.of<AppContext>(context, listen: false);
final api = (appContext.getContext() as ManagerAppContext).clientAPI!.sectionApi!;
try {
final sections = await api.sectionGetFromConfiguration(eventDTO.configurationId!);
if (sections == null || !mounted) return;
setState(() {
availableMaps = sections.where((s) => s.type == SectionType.Map).toList();
});
} catch (e) {
// Silently keep empty on error
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -182,6 +220,12 @@ class _EventConfigState extends State<EventConfig> {
), ),
), ),
Divider(), Divider(),
// --- Carte de base ---
_buildBaseSectionMapSection(),
Divider(),
// --- Annotations globales ---
_buildGlobalAnnotationsSection(),
Divider(),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row( child: Row(
@ -383,7 +427,170 @@ class _EventConfigState extends State<EventConfig> {
}, },
), ),
), ),
Divider(),
// --- Parcours ---
if (eventDTO.id != null)
ParcoursConfig(
initialValue: const [],
parentId: eventDTO.id!,
isEvent: true,
onChanged: (paths) {
// parcours are managed independently, no DTO update needed here
},
),
], ],
); );
} }
Widget _buildBaseSectionMapSection() {
final mapItems = <String>['Aucune', ...availableMaps.map((m) => m.label ?? m.id ?? '')];
final currentValue = eventDTO.baseSectionMapId != null
? (availableMaps
.where((m) => m.id == eventDTO.baseSectionMapId)
.map((m) => m.label ?? m.id ?? '')
.firstOrNull ??
'Aucune')
: 'Aucune';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Carte de base",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
DropDownInputContainer(
label: "Carte :",
values: mapItems,
initialValue: currentValue,
onChange: (val) {
setState(() {
if (val == 'Aucune') {
eventDTO.baseSectionMapId = null;
} else {
final match = availableMaps.firstWhere(
(m) => (m.label ?? m.id ?? '') == val,
orElse: () => availableMaps.first);
eventDTO.baseSectionMapId = match.id;
}
widget.onChanged(eventDTO);
});
},
),
],
),
);
}
Widget _buildGlobalAnnotationsSection() {
final annotations = eventDTO.globalMapAnnotations ?? [];
final appContext = Provider.of<AppContext>(context, listen: false);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Annotations globales",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text("Ajouter une annotation"),
onPressed: eventDTO.id == null
? null
: () {
showNewOrUpdateMapAnnotation(context, null,
(newAnnotation) async {
try {
final created = await (appContext.getContext()
as ManagerAppContext)
.clientAPI!
.sectionEventApi!
.sectionEventCreateGlobalMapAnnotation(
eventDTO.id!, newAnnotation);
if (created != null && mounted) {
setState(() {
eventDTO.globalMapAnnotations = [
...annotations,
created
];
});
showNotification(kSuccess, kWhite,
'Annotation créée avec succès', context, null);
}
} catch (e) {
showNotification(kError, kWhite,
'Erreur lors de la création de l\'annotation',
context, null);
}
});
},
style: ElevatedButton.styleFrom(
backgroundColor: kSuccess,
foregroundColor: kWhite,
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
],
),
const SizedBox(height: 8),
if (annotations.isEmpty)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text("Aucune annotation globale",
style: TextStyle(fontStyle: FontStyle.italic)),
)
else
...annotations.asMap().entries.map((entry) {
final idx = entry.key;
final ann = entry.value;
final labelText = ann.label != null && ann.label!.isNotEmpty
? (ann.label!.firstWhere((t) => t.language == 'FR',
orElse: () => ann.label![0])
.value ?? 'Annotation ${idx + 1}')
: 'Annotation ${idx + 1}';
return Card(
margin:
const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
child: ListTile(
leading: const Icon(Icons.place, color: kPrimaryColor),
title: Text(labelText),
trailing: IconButton(
icon: const Icon(Icons.delete, color: kError),
onPressed: () async {
try {
if (ann.id != null) {
await (appContext.getContext() as ManagerAppContext)
.clientAPI!
.sectionEventApi!
.sectionEventDeleteMapAnnotation(ann.id!);
}
if (mounted) {
setState(() {
final updated = List<MapAnnotationDTO>.from(
eventDTO.globalMapAnnotations ?? []);
updated.removeAt(idx);
eventDTO.globalMapAnnotations = updated;
});
showNotification(kSuccess, kWhite,
'Annotation supprimée', context, null);
}
} catch (e) {
showNotification(kError, kWhite,
'Erreur lors de la suppression', context, null);
}
},
),
),
);
}),
],
),
);
}
} }

View File

@ -0,0 +1,180 @@
import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart';
import 'package:manager_app/constants.dart';
import 'package:manager_app/Components/rounded_button.dart';
import 'package:manager_app/Components/multi_string_input_container.dart';
import 'package:manager_app/Components/string_input_container.dart';
import 'package:manager_app/Components/dropDown_input_container.dart';
void showNewOrUpdateMapAnnotation(
BuildContext context,
MapAnnotationDTO? annotation,
Function(MapAnnotationDTO) onSave,
) {
MapAnnotationDTO working = annotation != null
? MapAnnotationDTO(
id: annotation.id,
label: List<TranslationDTO>.from(annotation.label ?? []),
type: annotation.type != null ? List<TranslationDTO>.from(annotation.type!) : [],
geometryType: annotation.geometryType,
polyColor: annotation.polyColor,
icon: annotation.icon,
)
: MapAnnotationDTO(
label: [],
type: [],
geometryType: GeometryType.number0, // Point
polyColor: '#FF0000',
);
final List<String> geometryTypeLabels = ['Point', 'Polyline', 'Circle', 'Polygon'];
String geometryTypeToLabel(GeometryType? gt) {
if (gt == null) return 'Point';
switch (gt.value) {
case 0: return 'Point';
case 1: return 'Polyline';
case 2: return 'Circle';
case 3: return 'Polygon';
default: return 'Point';
}
}
GeometryType labelToGeometryType(String label) {
switch (label) {
case 'Polyline': return GeometryType.number1;
case 'Circle': return GeometryType.number2;
case 'Polygon': return GeometryType.number3;
default: return GeometryType.number0;
}
}
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
final double screenWidth = MediaQuery.of(context).size.width;
final double screenHeight = MediaQuery.of(context).size.height;
final double dialogWidth = screenWidth * 0.6;
final double contentWidth = dialogWidth - 48;
final double halfWidth = (contentWidth - 20) / 2;
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
width: dialogWidth,
constraints: BoxConstraints(maxHeight: screenHeight * 0.8),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
annotation == null ? "Nouvelle Annotation" : "Modifier l'Annotation",
style: TextStyle(
color: kPrimaryColor,
fontSize: 20,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Flexible(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
SizedBox(
width: halfWidth,
child: MultiStringInputContainer(
label: "Label :",
modalLabel: "Label de l'annotation",
initialValue: working.label ?? [],
onGetResult: (val) =>
setState(() => working.label = val),
maxLines: 1,
isTitle: true,
isHTML: false,
),
),
const SizedBox(width: 20),
SizedBox(
width: halfWidth,
child: DropDownInputContainer(
label: "Géométrie :",
values: geometryTypeLabels,
initialValue: geometryTypeToLabel(working.geometryType),
onChange: (val) => setState(() =>
working.geometryType = labelToGeometryType(val)),
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
SizedBox(
width: halfWidth,
child: StringInputContainer(
label: "Couleur (hex) :",
initialValue: working.polyColor ?? '#FF0000',
onChanged: (val) =>
setState(() => working.polyColor = val),
),
),
const SizedBox(width: 20),
SizedBox(
width: halfWidth,
child: StringInputContainer(
label: "Icône (material) :",
initialValue: working.icon ?? '',
onChanged: (val) =>
setState(() => working.icon = val.isEmpty ? null : val),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: 46,
child: RoundedButton(
text: "Annuler",
press: () => Navigator.pop(context),
color: kSecond,
fontSize: 15,
horizontal: 24,
),
),
const SizedBox(width: 12),
SizedBox(
height: 46,
child: RoundedButton(
text: "Sauvegarder",
press: () {
onSave(working);
Navigator.pop(context);
},
color: kPrimaryColor,
fontSize: 15,
horizontal: 24,
),
),
],
),
],
),
),
);
},
);
},
);
}

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
import 'package:manager_app/Components/confirmation_dialog.dart';
import 'package:manager_app/Components/rounded_button.dart'; import 'package:manager_app/Components/rounded_button.dart';
import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'showNewOrUpdateMapAnnotation.dart';
void showNewOrUpdateProgrammeBlock( void showNewOrUpdateProgrammeBlock(
BuildContext context, BuildContext context,
@ -17,14 +19,21 @@ void showNewOrUpdateProgrammeBlock(
description: List<TranslationDTO>.from(block.description ?? []), description: List<TranslationDTO>.from(block.description ?? []),
startTime: block.startTime, startTime: block.startTime,
endTime: block.endTime, endTime: block.endTime,
mapAnnotations: List<MapAnnotation>.from(block.mapAnnotations ?? []),
) )
: ProgrammeBlock( : ProgrammeBlock(
title: [], title: [],
description: [], description: [],
startTime: DateTime.now(), startTime: DateTime.now(),
endTime: DateTime.now().add(Duration(hours: 1)), endTime: DateTime.now().add(Duration(hours: 1)),
mapAnnotations: [],
); );
MapAnnotationDTO _toDTO(MapAnnotation a) =>
MapAnnotationDTO.fromJson(a.toJson())!;
MapAnnotation _fromDTO(MapAnnotationDTO a) =>
MapAnnotation.fromJson(a.toJson())!;
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
@ -198,6 +207,119 @@ void showNewOrUpdateProgrammeBlock(
), ),
], ],
), ),
Divider(height: 24),
// Annotations
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Annotations",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15)),
IconButton(
icon: Icon(Icons.add_circle_outline,
color: kSuccess),
onPressed: () {
showNewOrUpdateMapAnnotation(
context,
null,
(newAnnotation) {
setState(() {
workingBlock.mapAnnotations = [
...(workingBlock.mapAnnotations ?? []),
_fromDTO(newAnnotation),
];
});
},
);
},
),
],
),
if (workingBlock.mapAnnotations == null ||
workingBlock.mapAnnotations!.isEmpty)
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: Text(
"Aucune annotation configurée.",
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey[600]),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount:
workingBlock.mapAnnotations!.length,
itemBuilder: (context, aIndex) {
final annotation =
workingBlock.mapAnnotations![aIndex];
final label = annotation.label
?.firstWhere(
(t) => t.language == 'FR',
orElse: () =>
annotation.label!.first,
)
.value ??
"Annotation $aIndex";
return ListTile(
dense: true,
title: Text(label),
subtitle: Text(
annotation.geometryType?.value == 1
? 'Polyline'
: annotation.geometryType
?.value ==
2
? 'Circle'
: annotation.geometryType
?.value ==
3
? 'Polygon'
: 'Point'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.edit,
size: 18,
color: kPrimaryColor),
onPressed: () {
showNewOrUpdateMapAnnotation(
context,
_toDTO(annotation),
(updated) {
setState(() {
workingBlock.mapAnnotations![
aIndex] =
_fromDTO(updated);
});
},
);
},
),
IconButton(
icon: Icon(Icons.delete,
size: 18, color: kError),
onPressed: () {
showConfirmationDialog(
"Supprimer cette annotation ?",
() {},
() => setState(() => workingBlock
.mapAnnotations!
.removeAt(aIndex)),
context,
);
},
),
],
),
);
},
),
], ],
), ),
), ),

View File

@ -13,6 +13,7 @@ import 'package:manager_app/Components/fetch_section_icon.dart';
import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_app/Components/resource_input_container.dart';
import 'package:manager_app/Components/multi_select_container.dart'; import 'package:manager_app/Components/multi_select_container.dart';
import 'package:manager_app/Components/single_select_container.dart'; import 'package:manager_app/Components/single_select_container.dart';
import 'package:manager_app/Components/check_input_container.dart';
import 'package:manager_app/Components/slider_input_container.dart'; import 'package:manager_app/Components/slider_input_container.dart';
import 'package:manager_app/Components/string_input_container.dart'; import 'package:manager_app/Components/string_input_container.dart';
import 'package:manager_app/Screens/Configurations/Section/SubSection/Map/showNewOrUpdateGeoPoint.dart'; import 'package:manager_app/Screens/Configurations/Section/SubSection/Map/showNewOrUpdateGeoPoint.dart';
@ -662,6 +663,16 @@ class _MapConfigState extends State<MapConfig> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
CheckInputContainer(
label: "Vue liste :",
isChecked: mapDTO.isListViewEnabled ?? false,
onChanged: (value) {
setState(() {
mapDTO.isListViewEnabled = value;
widget.onChanged(mapDTO);
});
},
),
if (mapDTO.mapProvider == MapProvider.Google) if (mapDTO.mapProvider == MapProvider.Google)
DropDownInputContainer( DropDownInputContainer(
label: "Type :", label: "Type :",

View File

@ -6,6 +6,7 @@ import 'package:manager_app/Components/confirmation_dialog.dart';
import 'package:manager_app/Components/geometry_input_container.dart'; import 'package:manager_app/Components/geometry_input_container.dart';
import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart';
import 'package:manager_app/Components/rounded_button.dart'; import 'package:manager_app/Components/rounded_button.dart';
import 'package:manager_app/Components/string_input_container.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
import 'showNewOrUpdateQuizQuestion.dart'; import 'showNewOrUpdateQuizQuestion.dart';
@ -108,6 +109,78 @@ void showNewOrUpdateGuidedStep(
}); });
}, },
), ),
Divider(height: 24),
// Comportement de l'étape
Text("Comportement", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
SizedBox(height: 8),
Row(
children: [
Expanded(
child: SwitchListTile(
dense: true,
title: Text("Cachée initialement"),
value: workingStep.isHiddenInitially ?? false,
onChanged: (val) => setState(() => workingStep.isHiddenInitially = val),
activeThumbColor: kPrimaryColor,
),
),
Expanded(
child: SwitchListTile(
dense: true,
title: Text("Verrouillée"),
value: workingStep.isStepLocked ?? false,
onChanged: (val) => setState(() => workingStep.isStepLocked = val),
activeThumbColor: kPrimaryColor,
),
),
Expanded(
child: SwitchListTile(
dense: true,
title: Text("Timer"),
value: workingStep.isStepTimer ?? false,
onChanged: (val) => setState(() => workingStep.isStepTimer = val),
activeThumbColor: kPrimaryColor,
),
),
],
),
if (workingStep.isStepTimer == true) ...[
SizedBox(height: 8),
Row(
children: [
SizedBox(
width: halfWidth,
child: StringInputContainer(
label: "Durée (secondes) :",
initialValue: workingStep.timerSeconds?.toString() ?? "",
onChanged: (val) => setState(() =>
workingStep.timerSeconds = int.tryParse(val)),
),
),
SizedBox(width: 20),
SizedBox(
width: halfWidth,
child: MultiStringInputContainer(
label: "Message d'expiration :",
modalLabel: "Message d'expiration du timer",
initialValue: workingStep.timerExpiredMessage ?? [],
onGetResult: (val) => setState(() =>
workingStep.timerExpiredMessage = val.isEmpty ? null : val),
maxLines: 2,
isTitle: false,
isHTML: false,
),
),
],
),
],
SizedBox(height: 8),
StringInputContainer(
label: "GeoPoint de déclenchement (ID) :",
initialValue: workingStep.triggerGeoPointId?.toString() ?? "",
onChanged: (val) => setState(() =>
workingStep.triggerGeoPointId = int.tryParse(val)),
),
// Questions uniquement en mode Escape Game // Questions uniquement en mode Escape Game
if (isEscapeMode) ...[ if (isEscapeMode) ...[
Divider(height: 24), Divider(height: 24),

View File

@ -391,6 +391,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
} }
getButtons(SectionDTO sectionDTO, AppContext appContext) { getButtons(SectionDTO sectionDTO, AppContext appContext) {
final canEdit = (appContext.getContext() as ManagerAppContext).canEdit;
return Align( return Align(
alignment: AlignmentDirectional.bottomCenter, alignment: AlignmentDirectional.bottomCenter,
child: Row( child: Row(
@ -409,7 +410,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
}, },
), ),
), ),
Padding( if (canEdit) Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: RoundedButton( child: RoundedButton(
text: "Supprimer", text: "Supprimer",
@ -422,7 +423,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
}, },
), ),
), ),
Padding( if (canEdit) Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: RoundedButton( child: RoundedButton(
text: "Sauvegarder", text: "Sauvegarder",

View File

@ -346,6 +346,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
} }
getButtons(ConfigurationDTO configurationDTO, AppContext appContext) { getButtons(ConfigurationDTO configurationDTO, AppContext appContext) {
final canEdit = (appContext.getContext() as ManagerAppContext).canEdit;
return Align( return Align(
alignment: AlignmentDirectional.bottomCenter, alignment: AlignmentDirectional.bottomCenter,
child: Row( child: Row(
@ -364,7 +365,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
}, },
), ),
), ),
Padding( if (canEdit) Padding(
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
child: RoundedButton( child: RoundedButton(
text: "Supprimer", text: "Supprimer",
@ -377,7 +378,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
}, },
), ),
), ),
Padding( if (canEdit) Padding(
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
child: RoundedButton( child: RoundedButton(
text: "Sauvegarder", text: "Sauvegarder",

View File

@ -43,7 +43,7 @@ class _ConfigurationsScreenState extends State<ConfigurationsScreen> {
builder: (context, AsyncSnapshot<dynamic> snapshot) { builder: (context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
var tempOutput = new List<ConfigurationDTO>.from(snapshot.data); var tempOutput = new List<ConfigurationDTO>.from(snapshot.data);
tempOutput.add(ConfigurationDTO(id: null)); if (managerAppContext.canEdit) tempOutput.add(ConfigurationDTO(id: null));
return bodyGrid(tempOutput, size, appContext, context); return bodyGrid(tempOutput, size, appContext, context);
} else if (snapshot.connectionState == ConnectionState.none) { } else if (snapshot.connectionState == ConnectionState.none) {
return Text("No data"); return Text("No data");

View File

@ -14,6 +14,7 @@ import 'package:manager_app/Screens/Kiosk_devices/kiosk_screen.dart';
import 'package:manager_app/Screens/Resources/resources_screen.dart'; import 'package:manager_app/Screens/Resources/resources_screen.dart';
import 'package:manager_app/Screens/Statistics/statistics_screen.dart'; import 'package:manager_app/Screens/Statistics/statistics_screen.dart';
import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart'; import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart';
import 'package:manager_app/Screens/Notifications/notifications_screen.dart';
import 'package:manager_app/Screens/Users/users_screen.dart'; import 'package:manager_app/Screens/Users/users_screen.dart';
import 'package:manager_app/app_context.dart'; import 'package:manager_app/app_context.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
@ -40,6 +41,11 @@ class _MainScreenState extends State<MainScreen> {
final ValueNotifier<int?> currentPosition = ValueNotifier<int?>(null); final ValueNotifier<int?> currentPosition = ValueNotifier<int?>(null);
// Keys for nudge animation one per section type
final _nudgeKeys = <String, GlobalKey<_NudgeIconState>>{};
GlobalKey<_NudgeIconState> _nudgeKey(String type) =>
_nudgeKeys.putIfAbsent(type, () => GlobalKey<_NudgeIconState>());
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -115,7 +121,7 @@ class _MainScreenState extends State<MainScreen> {
onTap: isCurrent onTap: isCurrent
? null ? null
: () async { : () async {
Navigator.pop(context); Navigator.of(context, rootNavigator: true).pop();
final newInstance = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id); final newInstance = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id);
if (newInstance != null && context.mounted) { if (newInstance != null && context.mounted) {
setState(() { setState(() {
@ -135,12 +141,25 @@ class _MainScreenState extends State<MainScreen> {
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Fermer')), TextButton(onPressed: () => Navigator.of(context, rootNavigator: true).pop(), child: const Text('Fermer')),
], ],
), ),
); );
} }
static IconData _sectionIcon(String type) {
switch (type) {
case 'devices': return Icons.apps;
case 'configurations': return Icons.settings_outlined;
case 'resources': return Icons.folder_open_outlined;
case 'statistics': return Icons.bar_chart;
case 'notifications': return Icons.notifications_none;
case 'users': return Icons.people_outline;
case 'apikeys': return Icons.vpn_key_outlined;
default: return Icons.circle_outlined;
}
}
Widget buildMenu(BuildContext context, AppContext appContext, ManagerAppContext managerAppContext, bool isDrawer) { Widget buildMenu(BuildContext context, AppContext appContext, ManagerAppContext managerAppContext, bool isDrawer) {
return Container( return Container(
width: isDrawer ? null : 250, // fixed width on sidebar, null on drawer for full width width: isDrawer ? null : 250, // fixed width on sidebar, null on drawer for full width
@ -190,6 +209,7 @@ class _MainScreenState extends State<MainScreen> {
if (section.subMenu.isEmpty) { if (section.subMenu.isEmpty) {
return Container( return Container(
key: ValueKey(section.type),
decoration: currentPath!.contains(section.type) decoration: currentPath!.contains(section.type)
? BoxDecoration( ? BoxDecoration(
border: Border( border: Border(
@ -201,21 +221,38 @@ class _MainScreenState extends State<MainScreen> {
) )
: null, : null,
child: ListTile( child: ListTile(
leading: _NudgeIcon(
key: _nudgeKey(section.type),
icon: _sectionIcon(section.type),
isActive: currentPath.contains(section.type),
color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor,
size: 22,
),
title: Text(section.name, style: TextStyle(color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: currentPath.contains(section.type) ? FontWeight.w500 : FontWeight.w100)), title: Text(section.name, style: TextStyle(color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: currentPath.contains(section.type) ? FontWeight.w500 : FontWeight.w100)),
selected: currentPosition == section.menuId, selected: currentPosition == section.menuId,
onTap: () { onTap: () {
//currentPosition.value = section.menuId; WidgetsBinding.instance.addPostFrameCallback((_) =>
_nudgeKey(section.type).currentState?.nudge());
context.go('/main/${section.type}'); context.go('/main/${section.type}');
if (isDrawer) Navigator.of(context).pop(); // Close drawer on mobile if (isDrawer) Navigator.of(context).pop();
}, },
), ),
); );
} else { } else {
final isAppActive = currentPath!.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr");
return Container( return Container(
key: ValueKey(section.type),
child: ExpansionTile( child: ExpansionTile(
iconColor: currentPath!.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? kPrimaryColor : kBodyTextColor, leading: _NudgeIcon(
collapsedIconColor: currentPath.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? kPrimaryColor : kBodyTextColor, key: _nudgeKey(section.type),
title: Text(section.name, style: TextStyle(color: currentPath.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: currentPath.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? FontWeight.w500 : FontWeight.w100)), icon: _sectionIcon(section.type),
isActive: isAppActive,
color: isAppActive ? kPrimaryColor : kBodyTextColor,
size: 22,
),
iconColor: isAppActive ? kPrimaryColor : kBodyTextColor,
collapsedIconColor: isAppActive ? kPrimaryColor : kBodyTextColor,
title: Text(section.name, style: TextStyle(color: isAppActive ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: isAppActive ? FontWeight.w500 : FontWeight.w100)),
children: section.subMenu.map((subSection) { children: section.subMenu.map((subSection) {
return Container( return Container(
decoration: currentPath.contains(subSection.type) decoration: currentPath.contains(subSection.type)
@ -246,11 +283,11 @@ class _MainScreenState extends State<MainScreen> {
), ),
selected: currentPosition.value == subSection.menuId, selected: currentPosition.value == subSection.menuId,
onTap: () { onTap: () {
if (currentPath != null && currentPath.contains(subSection.type)) { if (currentPath != null && currentPath.contains(subSection.type)) {
// DO NOTHING, we are already display the correct interface // already on this page
} else { } else {
WidgetsBinding.instance.addPostFrameCallback((_) =>
_nudgeKey(section.type).currentState?.nudge());
context.go('/main/${subSection.type}'); context.go('/main/${subSection.type}');
} }
if (isDrawer) Navigator.of(context).pop(); if (isDrawer) Navigator.of(context).pop();
@ -316,6 +353,12 @@ class _MainScreenState extends State<MainScreen> {
// Synchronise les items de menu sensibles au rôle à chaque rebuild // Synchronise les items de menu sensibles au rôle à chaque rebuild
final role = managerAppContext.role; final role = managerAppContext.role;
final hasAdminItems = menu.sections!.any((s) => s.menuId == 8); final hasAdminItems = menu.sections!.any((s) => s.menuId == 8);
final hasNotifItem = menu.sections!.any((s) => s.menuId == 10);
if (role != null && role.value <= 1 && managerAppContext.instanceDTO?.isPushNotification == true && !hasNotifItem) {
menu.sections!.add(MenuSection(name: "Notifications", type: "notifications", menuId: 10, subMenu: []));
} else if ((role == null || role.value > 1 || managerAppContext.instanceDTO?.isPushNotification != true) && hasNotifItem) {
menu.sections!.removeWhere((s) => s.menuId == 10);
}
if (role != null && role.value <= 1 && !hasAdminItems) { if (role != null && role.value <= 1 && !hasAdminItems) {
menu.sections!.add(MenuSection(name: "Utilisateurs", type: "users", menuId: 8, subMenu: [])); menu.sections!.add(MenuSection(name: "Utilisateurs", type: "users", menuId: 8, subMenu: []));
menu.sections!.add(MenuSection(name: "Clés API", type: "apikeys", menuId: 9, subMenu: [])); menu.sections!.add(MenuSection(name: "Clés API", type: "apikeys", menuId: 9, subMenu: []));
@ -403,6 +446,9 @@ class _MainScreenState extends State<MainScreen> {
case "apikeys": case "apikeys":
currentPosition = 9; currentPosition = 9;
break; break;
case "notifications":
currentPosition = 10;
break;
} }
} }
@ -473,7 +519,59 @@ class _MainScreenState extends State<MainScreen> {
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: ApiKeysScreen() child: ApiKeysScreen()
); );
case 'notifications':
return const Padding(
padding: EdgeInsets.all(8.0),
child: NotificationsScreen()
);
default: default:
return Text('Hellow default'); return Text('Hellow default');
} }
} }
class _NudgeIcon extends StatefulWidget {
final IconData icon;
final bool isActive;
final Color color;
final double size;
const _NudgeIcon({Key? key, required this.icon, required this.isActive, required this.color, required this.size}) : super(key: key);
@override
State<_NudgeIcon> createState() => _NudgeIconState();
}
class _NudgeIconState extends State<_NudgeIcon> with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<double> _dx;
@override
void initState() {
super.initState();
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
_dx = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0.0, end: 5.0), weight: 35),
TweenSequenceItem(tween: Tween(begin: 5.0, end: 0.0), weight: 65),
]).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
}
void nudge() => _ctrl.forward(from: 0);
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _dx,
builder: (_, child) => Transform.translate(
offset: Offset(_dx.value, 0),
child: child,
),
child: Icon(widget.icon, color: widget.color, size: widget.size),
);
}
}

View File

@ -0,0 +1,454 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/app_context.dart';
import 'package:manager_app/constants.dart';
import 'package:manager_api_new/api.dart';
import 'package:provider/provider.dart';
class NotificationsScreen extends StatefulWidget {
const NotificationsScreen({Key? key}) : super(key: key);
@override
_NotificationsScreenState createState() => _NotificationsScreenState();
}
class _NotificationsScreenState extends State<NotificationsScreen> {
List<PushNotificationDTO> _notifications = [];
bool _loading = true;
String? _statusFilter; // null = toutes
int _page = 0;
static const _pageSize = 15;
// KPI counts
int get _totalCount => _notifications.length;
int get _sentCount => _notifications.where((n) => n.status == 'Sent').length;
int get _scheduledCount => _notifications.where((n) => n.status == 'Scheduled').length;
int get _failedCount => _notifications.where((n) => n.status == 'Failed').length;
// Filtre + pagination
List<PushNotificationDTO> get _filtered => _statusFilter == null
? _notifications
: _notifications.where((n) => n.status == _statusFilter).toList();
int get _pageCount => (_filtered.isEmpty ? 1 : (_filtered.length / _pageSize).ceil());
List<PushNotificationDTO> get _paginated =>
_filtered.skip(_page * _pageSize).take(_pageSize).toList();
void _setFilter(String? status) {
setState(() {
_statusFilter = _statusFilter == status ? null : status;
_page = 0;
});
}
// API helpers
Future<void> _load(ManagerAppContext ctx) async {
try {
final response = await ctx.clientAPI!.notificationApi!.notificationGetWithHttpInfo();
if (response.statusCode == 200) {
final List<dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
setState(() {
_notifications = PushNotificationDTO.listFromJson(json);
_loading = false;
});
}
} catch (e) {
setState(() => _loading = false);
}
}
Future<bool> _send(ManagerAppContext ctx, String title, String body) async {
final response = await ctx.clientAPI!.notificationApi!.notificationSendWithHttpInfo(
SendNotificationRequest(title: title, body: body),
);
return response.statusCode == 200;
}
Future<bool> _schedule(ManagerAppContext ctx, String title, String body, DateTime scheduledAt) async {
final response = await ctx.clientAPI!.notificationApi!.notificationScheduleWithHttpInfo(
ScheduleNotificationRequest(title: title, body: body, scheduledAt: scheduledAt),
);
return response.statusCode == 200;
}
Future<bool> _cancel(ManagerAppContext ctx, String id) async {
final response = await ctx.clientAPI!.notificationApi!.notificationCancelWithHttpInfo(id);
return response.statusCode == 202;
}
// Dialogs
void _showSendModal(BuildContext context, ManagerAppContext ctx) {
final titleCtrl = TextEditingController();
final bodyCtrl = TextEditingController();
bool isScheduled = false;
DateTime? scheduledDate;
TimeOfDay? scheduledTime;
showDialog(
context: context,
builder: (_) => StatefulBuilder(builder: (ctx2, setLocal) {
return AlertDialog(
title: const Text('Nouveau message'),
content: SizedBox(
width: 480,
child: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
controller: titleCtrl,
decoration: const InputDecoration(labelText: 'Titre'),
),
const SizedBox(height: 8),
TextField(
controller: bodyCtrl,
decoration: const InputDecoration(labelText: 'Message'),
maxLines: 3,
),
const SizedBox(height: 16),
Row(
children: [
Checkbox(
value: isScheduled,
onChanged: (v) => setLocal(() => isScheduled = v ?? false),
),
const Text('Planifier'),
],
),
if (isScheduled) ...[
const SizedBox(height: 8),
Row(children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.calendar_today, size: 16),
label: Text(scheduledDate != null
? '${scheduledDate!.day.toString().padLeft(2, '0')}/${scheduledDate!.month.toString().padLeft(2, '0')}/${scheduledDate!.year}'
: 'Date'),
onPressed: () async {
final d = await showDatePicker(
context: ctx2,
initialDate: DateTime.now().add(const Duration(hours: 1)),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (d != null) setLocal(() => scheduledDate = d);
},
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.access_time, size: 16),
label: Text(scheduledTime != null
? scheduledTime!.format(ctx2)
: 'Heure'),
onPressed: () async {
final t = await showTimePicker(
context: ctx2,
initialTime: TimeOfDay.now(),
);
if (t != null) setLocal(() => scheduledTime = t);
},
),
),
]),
],
]),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx2),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: titleCtrl.text.isEmpty ? null : () async {
Navigator.pop(ctx2);
bool ok;
if (isScheduled && scheduledDate != null && scheduledTime != null) {
final dt = DateTime(
scheduledDate!.year,
scheduledDate!.month,
scheduledDate!.day,
scheduledTime!.hour,
scheduledTime!.minute,
);
ok = await _schedule(ctx, titleCtrl.text, bodyCtrl.text, dt);
} else {
ok = await _send(ctx, titleCtrl.text, bodyCtrl.text);
}
if (ok && context.mounted) {
await _load(ctx);
}
},
child: const Text('Envoyer'),
),
],
);
}),
);
}
void _confirmCancel(BuildContext context, ManagerAppContext ctx, PushNotificationDTO notif) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Annuler la notification'),
content: Text('Annuler « ${notif.title} » ?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Non')),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () async {
Navigator.pop(context);
final ok = await _cancel(ctx, notif.id!);
if (ok && context.mounted) await _load(ctx);
},
child: const Text('Annuler', style: TextStyle(color: Colors.white)),
),
],
),
);
}
// Helpers
static String _formatDate(DateTime? dt) {
if (dt == null) return '';
final d = dt.toLocal();
return '${d.day.toString().padLeft(2, '0')}/${d.month.toString().padLeft(2, '0')}/${d.year} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}';
}
static Widget _statusChip(String status) {
Color bg;
Color fg;
switch (status) {
case 'Sent':
bg = Colors.green.shade50;
fg = Colors.green.shade700;
break;
case 'Scheduled':
bg = Colors.blue.shade50;
fg = Colors.blue.shade700;
break;
case 'Failed':
bg = Colors.red.shade50;
fg = Colors.red.shade700;
break;
default:
bg = Colors.grey.shade100;
fg = Colors.grey.shade700;
}
return Chip(
label: Text(status, style: TextStyle(color: fg, fontSize: 12)),
backgroundColor: bg,
side: BorderSide(color: fg.withValues(alpha: 0.3)),
padding: const EdgeInsets.symmetric(horizontal: 4),
visualDensity: VisualDensity.compact,
);
}
// Build
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final ctx = Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
_load(ctx);
});
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
final ctx = appContext.getContext() as ManagerAppContext;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Notifications',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: kPrimaryColor)),
ElevatedButton.icon(
onPressed: () => _showSendModal(context, ctx),
icon: const Icon(Icons.add),
label: const Text('Nouveau message'),
),
],
),
const SizedBox(height: 16),
// KPIs
Row(
children: [
_KpiCard(
label: 'Toutes',
count: _totalCount,
color: Colors.blueGrey,
selected: _statusFilter == null,
onTap: () => _setFilter(null),
),
const SizedBox(width: 12),
_KpiCard(
label: 'Envoyées',
count: _sentCount,
color: Colors.green,
selected: _statusFilter == 'Sent',
onTap: () => _setFilter('Sent'),
),
const SizedBox(width: 12),
_KpiCard(
label: 'Planifiées',
count: _scheduledCount,
color: Colors.blue,
selected: _statusFilter == 'Scheduled',
onTap: () => _setFilter('Scheduled'),
),
const SizedBox(width: 12),
_KpiCard(
label: 'Échouées',
count: _failedCount,
color: Colors.red,
selected: _statusFilter == 'Failed',
onTap: () => _setFilter('Failed'),
),
],
),
const SizedBox(height: 16),
// List
if (_loading)
const Center(child: CircularProgressIndicator())
else if (_notifications.isEmpty)
const Center(child: Text('Aucune notification envoyée'))
else if (_filtered.isEmpty)
const Center(child: Text('Aucune notification pour ce filtre'))
else
Expanded(
child: Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.grey.shade200),
),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: SizedBox(
width: double.infinity,
child: DataTable(
horizontalMargin: 16,
columnSpacing: 24,
headingRowColor: WidgetStateProperty.all(Colors.grey.shade50),
dividerThickness: 1,
columns: const [
DataColumn(label: Text('Titre', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Topic', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Date', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Statut', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('')),
],
rows: _paginated.map((n) {
final isScheduled = n.status == 'Scheduled';
final date = isScheduled ? n.scheduledAt : n.sentAt;
return DataRow(cells: [
DataCell(Text(n.title ?? '')),
DataCell(Text(n.topic ?? '', style: const TextStyle(color: Colors.grey))),
DataCell(Text(_formatDate(date), style: const TextStyle(color: Colors.grey))),
DataCell(_statusChip(n.status ?? '')),
DataCell(isScheduled
? IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20),
tooltip: 'Annuler',
onPressed: () => _confirmCancel(context, ctx, n),
)
: const SizedBox()),
]);
}).toList(),
),
),
),
),
// Pagination
Container(
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Colors.grey.shade200)),
),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: _page > 0 ? () => setState(() => _page--) : null,
),
Text('${_page + 1} / $_pageCount', style: const TextStyle(fontSize: 13)),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _page < _pageCount - 1 ? () => setState(() => _page++) : null,
),
const SizedBox(width: 16),
Text(
'${_filtered.length} résultat${_filtered.length > 1 ? 's' : ''}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
],
),
),
),
],
);
}
}
class _KpiCard extends StatelessWidget {
final String label;
final int count;
final MaterialColor color;
final bool selected;
final VoidCallback onTap;
const _KpiCard({
required this.label,
required this.count,
required this.color,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 120,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: selected ? color.shade100 : color.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: selected ? color.shade400 : color.shade200,
width: selected ? 2 : 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$count',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: color.shade700)),
const SizedBox(height: 2),
Text(label,
style: TextStyle(fontSize: 12, color: color.shade600)),
],
),
),
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:manager_app/Components/fetch_resource_icon.dart'; import 'package:manager_app/Components/fetch_resource_icon.dart';
import 'package:manager_app/Components/multi_select_container.dart'; import 'package:manager_app/Components/multi_select_container.dart';
import 'package:manager_app/Components/string_input_container.dart'; import 'package:manager_app/Components/string_input_container.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/app_context.dart'; import 'package:manager_app/app_context.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
@ -89,7 +90,7 @@ class _ResourceBodyGridState extends State<ResourceBodyGrid> {
}, },
), ),
), ),
if (widget.isAddButton) if (widget.isAddButton && (appContext.getContext() as ManagerAppContext).canEdit)
InkWell( InkWell(
onTap: () { onTap: () {
widget.onSelect(ResourceDTO(id: widget.isSelectModal ? "-1" : null)); widget.onSelect(ResourceDTO(id: widget.isSelectModal ? "-1" : null));

View File

@ -17,14 +17,23 @@ class _UsersScreenState extends State<UsersScreen> {
List<Map<String, dynamic>> _users = []; List<Map<String, dynamic>> _users = [];
bool _loading = true; bool _loading = true;
static String _roleName(int? v) { static const _roleNames = ['SuperAdmin', 'InstanceAdmin', 'ContentEditor', 'Viewer'];
switch (v) {
case 0: return 'SuperAdmin'; static String _roleName(dynamic v) {
case 1: return 'InstanceAdmin'; if (v is String) return v;
case 2: return 'ContentEditor'; final i = v as int?;
case 3: return 'Viewer'; if (i != null && i >= 0 && i < _roleNames.length) return _roleNames[i];
default: return ''; return '';
} }
// Convertit une valeur de rôle (String ou int) en int pour les comparaisons
static int _roleToInt(dynamic v) {
if (v is int) return v;
if (v is String) {
final i = _roleNames.indexOf(v);
return i >= 0 ? i : 3;
}
return 3;
} }
// Roles the caller is allowed to assign (can't assign higher than own role) // Roles the caller is allowed to assign (can't assign higher than own role)
@ -79,7 +88,7 @@ class _UsersScreenState extends State<UsersScreen> {
} }
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) { void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
final callerRole = ctx.role?.value ?? 2; final callerRole = _roleToInt(ctx.role?.value);
final emailCtrl = TextEditingController(); final emailCtrl = TextEditingController();
final firstCtrl = TextEditingController(); final firstCtrl = TextEditingController();
final lastCtrl = TextEditingController(); final lastCtrl = TextEditingController();
@ -131,10 +140,10 @@ class _UsersScreenState extends State<UsersScreen> {
} }
void _showEditDialog(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> user) { void _showEditDialog(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> user) {
final callerRole = ctx.role?.value ?? 2; final callerRole = _roleToInt(ctx.role?.value);
final firstCtrl = TextEditingController(text: user['firstName'] as String? ?? ''); final firstCtrl = TextEditingController(text: user['firstName'] as String? ?? '');
final lastCtrl = TextEditingController(text: user['lastName'] as String? ?? ''); final lastCtrl = TextEditingController(text: user['lastName'] as String? ?? '');
int selectedRole = (user['role'] as int?) ?? callerRole; int selectedRole = _roleToInt(user['role']);
showDialog( showDialog(
context: context, context: context,
@ -208,6 +217,15 @@ class _UsersScreenState extends State<UsersScreen> {
}); });
} }
static Color _roleColor(dynamic v) {
switch (_roleToInt(v)) {
case 0: return Colors.purple;
case 1: return Colors.blue;
case 2: return Colors.teal;
default: return Colors.grey;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context); final appContext = Provider.of<AppContext>(context);
@ -234,28 +252,54 @@ class _UsersScreenState extends State<UsersScreen> {
const Center(child: Text('Aucun utilisateur')) const Center(child: Text('Aucun utilisateur'))
else else
Expanded( Expanded(
child: Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.grey.shade200),
),
clipBehavior: Clip.antiAlias,
child: SingleChildScrollView( child: SingleChildScrollView(
child: SizedBox(
width: double.infinity,
child: DataTable( child: DataTable(
horizontalMargin: 16,
columnSpacing: 24,
headingRowColor: WidgetStateProperty.all(Colors.grey.shade50),
dividerThickness: 1,
columns: const [ columns: const [
DataColumn(label: Text('Email')), DataColumn(label: Text('Email', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Prénom')), DataColumn(label: Text('Prénom', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Nom')), DataColumn(label: Text('Nom', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Rôle')), DataColumn(label: Text('Rôle', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Actions')), DataColumn(label: Text('Actions', style: TextStyle(fontWeight: FontWeight.w600))),
], ],
rows: _users.map((user) { rows: _users.map((user) {
final roleColor = _roleColor(user['role']);
return DataRow(cells: [ return DataRow(cells: [
DataCell(Text(user['email'] as String? ?? '')), DataCell(Text(user['email'] as String? ?? '')),
DataCell(Text(user['firstName'] as String? ?? '')), DataCell(Text(user['firstName'] as String? ?? '')),
DataCell(Text(user['lastName'] as String? ?? '')), DataCell(Text(user['lastName'] as String? ?? '')),
DataCell(Text(_roleName(user['role'] as int?))), DataCell(Chip(
label: Text(
_roleName(user['role']),
style: TextStyle(color: roleColor, fontSize: 12),
),
backgroundColor: roleColor.withValues(alpha: 0.08),
side: BorderSide(color: roleColor.withValues(alpha: 0.3)),
padding: const EdgeInsets.symmetric(horizontal: 4),
visualDensity: VisualDensity.compact,
)),
DataCell(Row(children: [ DataCell(Row(children: [
IconButton( IconButton(
icon: Icon(Icons.edit, color: kPrimaryColor), icon: Icon(Icons.edit, color: kPrimaryColor, size: 20),
tooltip: 'Modifier',
onPressed: () => _showEditDialog(context, managerCtx, user), onPressed: () => _showEditDialog(context, managerCtx, user),
), ),
IconButton( IconButton(
icon: const Icon(Icons.delete, color: Colors.red), icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20),
tooltip: 'Supprimer',
onPressed: () => _confirmDelete(context, managerCtx, user), onPressed: () => _confirmDelete(context, managerCtx, user),
), ),
])), ])),
@ -264,6 +308,8 @@ class _UsersScreenState extends State<UsersScreen> {
), ),
), ),
), ),
),
),
], ],
); );
} }

View File

@ -47,6 +47,9 @@ class Client {
ApiKeyApi? _apiKeyApi; ApiKeyApi? _apiKeyApi;
ApiKeyApi? get apiKeyApi => _apiKeyApi; ApiKeyApi? get apiKeyApi => _apiKeyApi;
NotificationApi? _notificationApi;
NotificationApi? get notificationApi => _notificationApi;
Client(String path) { Client(String path) {
_apiClient = ApiClient(basePath: path); _apiClient = ApiClient(basePath: path);
//basePath: "https://192.168.31.140"); //basePath: "https://192.168.31.140");
@ -66,5 +69,6 @@ class Client {
_sectionEventApi = SectionEventApi(_apiClient); _sectionEventApi = SectionEventApi(_apiClient);
_statsApi = StatsApi(_apiClient); _statsApi = StatsApi(_apiClient);
_apiKeyApi = ApiKeyApi(_apiClient); _apiKeyApi = ApiKeyApi(_apiClient);
_notificationApi = NotificationApi(_apiClient);
} }
} }

View File

@ -263,10 +263,15 @@ class _MyAppState extends State<MyApp> {
//const Locale('fr', 'FR'), //const Locale('fr', 'FR'),
],*/ ],*/
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.blue,
primaryColor: kPrimaryColor, primaryColor: kPrimaryColor,
scaffoldBackgroundColor: kBackgroundColor, scaffoldBackgroundColor: kBackgroundColor,
//fontFamily: "Vollkorn", colorScheme: ColorScheme.light(
primary: kPrimaryColor,
onPrimary: Colors.white,
secondary: kPrimaryColor,
onSecondary: Colors.white,
error: kError,
),
textTheme: TextTheme(bodyLarge: TextStyle(color: kBodyTextColor)), textTheme: TextTheme(bodyLarge: TextStyle(color: kBodyTextColor)),
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
), ),

View File

@ -41,9 +41,14 @@ part 'api/section_agenda_api.dart';
part 'api/section_event_api.dart'; part 'api/section_event_api.dart';
part 'api/section_map_api.dart'; part 'api/section_map_api.dart';
part 'api/section_quiz_api.dart'; part 'api/section_quiz_api.dart';
part 'api/notification_api.dart';
part 'api/stats_api.dart'; part 'api/stats_api.dart';
part 'api/user_api.dart'; part 'api/user_api.dart';
part 'model/push_notification_dto.dart';
part 'model/send_notification_request.dart';
part 'model/schedule_notification_request.dart';
part 'model/agenda_dto.dart'; part 'model/agenda_dto.dart';
part 'model/agenda_dto_all_of_agenda_map_provider.dart'; part 'model/agenda_dto_all_of_agenda_map_provider.dart';
part 'model/ai_card_dto.dart'; part 'model/ai_card_dto.dart';

View File

@ -0,0 +1,97 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class NotificationApi {
NotificationApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// GET /api/Notification list notifications for the current instance
Future<Response> notificationGetWithHttpInfo() async {
final path = r'/api/Notification';
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
null,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// POST /api/Notification/send send a notification immediately
Future<Response> notificationSendWithHttpInfo(
SendNotificationRequest sendNotificationRequest,
) async {
final path = r'/api/Notification/send';
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
sendNotificationRequest,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// POST /api/Notification/schedule schedule a notification
Future<Response> notificationScheduleWithHttpInfo(
ScheduleNotificationRequest scheduleNotificationRequest,
) async {
final path = r'/api/Notification/schedule';
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
scheduleNotificationRequest,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// DELETE /api/Notification/{id} cancel a scheduled notification
Future<Response> notificationCancelWithHttpInfo(String id) async {
final path = r'/api/Notification/{id}'.replaceAll('{id}', id);
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
null,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
}

View File

@ -193,6 +193,54 @@ class SectionAgendaApi {
return null; return null;
} }
/// Performs an HTTP 'GET /api/SectionAgenda/{sectionAgendaId}/events/upcoming' operation and returns the [Response].
/// Parameters:
///
/// * [String] sectionAgendaId (required):
Future<Response> sectionAgendaGetUpcomingEventsWithHttpInfo(
String sectionAgendaId,
) async {
final path = r'/api/SectionAgenda/{sectionAgendaId}/events/upcoming'
.replaceAll('{sectionAgendaId}', sectionAgendaId);
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] sectionAgendaId (required):
Future<List<EventAgendaDTO>?> sectionAgendaGetUpcomingEvents(
String sectionAgendaId,
) async {
final response = await sectionAgendaGetUpcomingEventsWithHttpInfo(sectionAgendaId);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<EventAgendaDTO>') as List)
.cast<EventAgendaDTO>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'PUT /api/SectionAgenda/event' operation and returns the [Response]. /// Performs an HTTP 'PUT /api/SectionAgenda/event' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///

View File

@ -662,7 +662,7 @@ class SectionApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] id (required): /// * [String] id (required):
Future<List<Object>?> sectionGetFromConfigurationDetail( Future<dynamic> sectionGetFromConfigurationDetail(
String id, String id,
) async { ) async {
final response = await sectionGetFromConfigurationDetailWithHttpInfo( final response = await sectionGetFromConfigurationDetailWithHttpInfo(
@ -671,16 +671,10 @@ class SectionApi {
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
// When a remote server returns no body with a status of 204, we shall not decode it. // Sections are polymorphic bypass generated deserialization and return raw JSON
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && if (response.body.isNotEmpty &&
response.statusCode != HttpStatus.noContent) { response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response); return json.decode(await _decodeBodyBytes(response));
return (await apiClient.deserializeAsync(responseBody, 'List<Object>')
as List)
.cast<Object>()
.toList(growable: false);
} }
return null; return null;
} }

View File

@ -481,4 +481,91 @@ class SectionEventApi {
} }
return null; return null;
} }
/// Performs an HTTP 'GET /api/SectionEvent/{sectionEventId}/global-map-annotations' operation and returns the [Response].
Future<Response> sectionEventGetGlobalMapAnnotationsWithHttpInfo(
String sectionEventId,
) async {
final path = r'/api/SectionEvent/{sectionEventId}/global-map-annotations'
.replaceAll('{sectionEventId}', sectionEventId);
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
null,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// GET global map annotations for a section event.
Future<List<MapAnnotationDTO>?> sectionEventGetGlobalMapAnnotations(
String sectionEventId,
) async {
final response = await sectionEventGetGlobalMapAnnotationsWithHttpInfo(sectionEventId);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<MapAnnotationDTO>') as List)
.cast<MapAnnotationDTO>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'POST /api/SectionEvent/{sectionEventId}/global-map-annotations' operation and returns the [Response].
Future<Response> sectionEventCreateGlobalMapAnnotationWithHttpInfo(
String sectionEventId,
MapAnnotationDTO mapAnnotationDTO,
) async {
final path = r'/api/SectionEvent/{sectionEventId}/global-map-annotations'
.replaceAll('{sectionEventId}', sectionEventId);
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
mapAnnotationDTO,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// POST create a global map annotation on a section event.
Future<MapAnnotationDTO?> sectionEventCreateGlobalMapAnnotation(
String sectionEventId,
MapAnnotationDTO mapAnnotationDTO,
) async {
final response = await sectionEventCreateGlobalMapAnnotationWithHttpInfo(
sectionEventId,
mapAnnotationDTO,
);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(
await _decodeBodyBytes(response),
'MapAnnotationDTO',
) as MapAnnotationDTO;
}
return null;
}
} }

View File

@ -16,6 +16,7 @@ class AiChatResponseNavigation {
this.sectionId, this.sectionId,
this.sectionTitle, this.sectionTitle,
this.sectionType, this.sectionType,
this.imageUrl,
}); });
String? sectionId; String? sectionId;
@ -24,6 +25,8 @@ class AiChatResponseNavigation {
String? sectionType; String? sectionType;
String? imageUrl;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@ -60,6 +63,11 @@ class AiChatResponseNavigation {
} else { } else {
json[r'sectionType'] = null; json[r'sectionType'] = null;
} }
if (this.imageUrl != null) {
json[r'imageUrl'] = this.imageUrl;
} else {
json[r'imageUrl'] = null;
}
return json; return json;
} }
@ -87,6 +95,7 @@ class AiChatResponseNavigation {
sectionId: mapValueOfType<String>(json, r'sectionId'), sectionId: mapValueOfType<String>(json, r'sectionId'),
sectionTitle: mapValueOfType<String>(json, r'sectionTitle'), sectionTitle: mapValueOfType<String>(json, r'sectionTitle'),
sectionType: mapValueOfType<String>(json, r'sectionType'), sectionType: mapValueOfType<String>(json, r'sectionType'),
imageUrl: mapValueOfType<String>(json, r'imageUrl'),
); );
} }
return null; return null;

View File

@ -15,6 +15,7 @@ class CreateApiKeyRequest {
CreateApiKeyRequest({ CreateApiKeyRequest({
this.name, this.name,
this.appType, this.appType,
this.dateExpiration,
}); });
String? name; String? name;
@ -27,21 +28,25 @@ class CreateApiKeyRequest {
/// ///
ApiKeyAppType? appType; ApiKeyAppType? appType;
DateTime? dateExpiration;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is CreateApiKeyRequest && other is CreateApiKeyRequest &&
other.name == name && other.name == name &&
other.appType == appType; other.appType == appType &&
other.dateExpiration == dateExpiration;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(name == null ? 0 : name!.hashCode) + (name == null ? 0 : name!.hashCode) +
(appType == null ? 0 : appType!.hashCode); (appType == null ? 0 : appType!.hashCode) +
(dateExpiration == null ? 0 : dateExpiration!.hashCode);
@override @override
String toString() => 'CreateApiKeyRequest[name=$name, appType=$appType]'; String toString() => 'CreateApiKeyRequest[name=$name, appType=$appType, dateExpiration=$dateExpiration]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -55,6 +60,11 @@ class CreateApiKeyRequest {
} else { } else {
json[r'appType'] = null; json[r'appType'] = null;
} }
if (this.dateExpiration != null) {
json[r'dateExpiration'] = this.dateExpiration!.toUtc().toIso8601String();
} else {
json[r'dateExpiration'] = null;
}
return json; return json;
} }
@ -81,6 +91,7 @@ class CreateApiKeyRequest {
return CreateApiKeyRequest( return CreateApiKeyRequest(
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),
appType: ApiKeyAppType.fromJson(json[r'appType']), appType: ApiKeyAppType.fromJson(json[r'appType']),
dateExpiration: mapDateTime(json, r'dateExpiration', r''),
); );
} }
return null; return null;

View File

@ -28,6 +28,11 @@ class EventAgendaDTO {
this.email, this.email,
this.sectionAgendaId, this.sectionAgendaId,
this.sectionEventId, this.sectionEventId,
this.isSynced,
this.idVideoYoutube,
this.videoLink,
this.videoResourceId,
this.videoResource,
}); });
/// ///
@ -66,6 +71,16 @@ class EventAgendaDTO {
String? sectionEventId; String? sectionEventId;
bool? isSynced;
String? idVideoYoutube;
String? videoLink;
String? videoResourceId;
EventAgendaDTOResource? videoResource;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@ -84,7 +99,12 @@ class EventAgendaDTO {
other.phone == phone && other.phone == phone &&
other.email == email && other.email == email &&
other.sectionAgendaId == sectionAgendaId && other.sectionAgendaId == sectionAgendaId &&
other.sectionEventId == sectionEventId; other.sectionEventId == sectionEventId &&
other.isSynced == isSynced &&
other.idVideoYoutube == idVideoYoutube &&
other.videoLink == videoLink &&
other.videoResourceId == videoResourceId &&
other.videoResource == videoResource;
@override @override
int get hashCode => int get hashCode =>
@ -103,7 +123,12 @@ class EventAgendaDTO {
(phone == null ? 0 : phone!.hashCode) + (phone == null ? 0 : phone!.hashCode) +
(email == null ? 0 : email!.hashCode) + (email == null ? 0 : email!.hashCode) +
(sectionAgendaId == null ? 0 : sectionAgendaId!.hashCode) + (sectionAgendaId == null ? 0 : sectionAgendaId!.hashCode) +
(sectionEventId == null ? 0 : sectionEventId!.hashCode); (sectionEventId == null ? 0 : sectionEventId!.hashCode) +
(isSynced == null ? 0 : isSynced!.hashCode) +
(idVideoYoutube == null ? 0 : idVideoYoutube!.hashCode) +
(videoLink == null ? 0 : videoLink!.hashCode) +
(videoResourceId == null ? 0 : videoResourceId!.hashCode) +
(videoResource == null ? 0 : videoResource!.hashCode);
@override @override
String toString() => String toString() =>
@ -186,6 +211,31 @@ class EventAgendaDTO {
} else { } else {
json[r'sectionEventId'] = null; json[r'sectionEventId'] = null;
} }
if (this.isSynced != null) {
json[r'isSynced'] = this.isSynced;
} else {
json[r'isSynced'] = null;
}
if (this.idVideoYoutube != null) {
json[r'idVideoYoutube'] = this.idVideoYoutube;
} else {
json[r'idVideoYoutube'] = null;
}
if (this.videoLink != null) {
json[r'videoLink'] = this.videoLink;
} else {
json[r'videoLink'] = null;
}
if (this.videoResourceId != null) {
json[r'videoResourceId'] = this.videoResourceId;
} else {
json[r'videoResourceId'] = null;
}
if (this.videoResource != null) {
json[r'videoResource'] = this.videoResource;
} else {
json[r'videoResource'] = null;
}
return json; return json;
} }
@ -225,6 +275,11 @@ class EventAgendaDTO {
email: mapValueOfType<String>(json, r'email'), email: mapValueOfType<String>(json, r'email'),
sectionAgendaId: mapValueOfType<String>(json, r'sectionAgendaId'), sectionAgendaId: mapValueOfType<String>(json, r'sectionAgendaId'),
sectionEventId: mapValueOfType<String>(json, r'sectionEventId'), sectionEventId: mapValueOfType<String>(json, r'sectionEventId'),
isSynced: mapValueOfType<bool>(json, r'isSynced'),
idVideoYoutube: mapValueOfType<String>(json, r'idVideoYoutube'),
videoLink: mapValueOfType<String>(json, r'videoLink'),
videoResourceId: mapValueOfType<String>(json, r'videoResourceId'),
videoResource: EventAgendaDTOResource.fromJson(json[r'videoResource']),
); );
} }
return null; return null;

View File

@ -32,6 +32,7 @@ class MapDTO {
this.meterZoneGPS, this.meterZoneGPS,
this.isBeacon, this.isBeacon,
this.beaconId, this.beaconId,
this.isListViewEnabled,
this.zoom, this.zoom,
this.mapType, this.mapType,
this.mapTypeMapbox, this.mapTypeMapbox,
@ -114,6 +115,8 @@ class MapDTO {
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
bool? isListViewEnabled;
int? zoom; int? zoom;
MapTypeApp? mapType; MapTypeApp? mapType;
@ -161,6 +164,7 @@ class MapDTO {
other.meterZoneGPS == meterZoneGPS && other.meterZoneGPS == meterZoneGPS &&
other.isBeacon == isBeacon && other.isBeacon == isBeacon &&
other.beaconId == beaconId && other.beaconId == beaconId &&
other.isListViewEnabled == isListViewEnabled &&
other.zoom == zoom && other.zoom == zoom &&
other.mapType == mapType && other.mapType == mapType &&
other.mapTypeMapbox == mapTypeMapbox && other.mapTypeMapbox == mapTypeMapbox &&
@ -196,6 +200,7 @@ class MapDTO {
(meterZoneGPS == null ? 0 : meterZoneGPS!.hashCode) + (meterZoneGPS == null ? 0 : meterZoneGPS!.hashCode) +
(isBeacon == null ? 0 : isBeacon!.hashCode) + (isBeacon == null ? 0 : isBeacon!.hashCode) +
(beaconId == null ? 0 : beaconId!.hashCode) + (beaconId == null ? 0 : beaconId!.hashCode) +
(isListViewEnabled == null ? 0 : isListViewEnabled!.hashCode) +
(zoom == null ? 0 : zoom!.hashCode) + (zoom == null ? 0 : zoom!.hashCode) +
(mapType == null ? 0 : mapType!.hashCode) + (mapType == null ? 0 : mapType!.hashCode) +
(mapTypeMapbox == null ? 0 : mapTypeMapbox!.hashCode) + (mapTypeMapbox == null ? 0 : mapTypeMapbox!.hashCode) +
@ -310,6 +315,11 @@ class MapDTO {
} else { } else {
json[r'beaconId'] = null; json[r'beaconId'] = null;
} }
if (this.isListViewEnabled != null) {
json[r'isListViewEnabled'] = this.isListViewEnabled;
} else {
json[r'isListViewEnabled'] = null;
}
if (this.zoom != null) { if (this.zoom != null) {
json[r'zoom'] = this.zoom; json[r'zoom'] = this.zoom;
} else { } else {
@ -413,6 +423,7 @@ class MapDTO {
meterZoneGPS: mapValueOfType<int>(json, r'meterZoneGPS'), meterZoneGPS: mapValueOfType<int>(json, r'meterZoneGPS'),
isBeacon: mapValueOfType<bool>(json, r'isBeacon'), isBeacon: mapValueOfType<bool>(json, r'isBeacon'),
beaconId: mapValueOfType<int>(json, r'beaconId'), beaconId: mapValueOfType<int>(json, r'beaconId'),
isListViewEnabled: mapValueOfType<bool>(json, r'isListViewEnabled'),
zoom: mapValueOfType<int>(json, r'zoom'), zoom: mapValueOfType<int>(json, r'zoom'),
mapType: MapTypeApp.fromJson(json[r'mapType']), mapType: MapTypeApp.fromJson(json[r'mapType']),
mapTypeMapbox: MapTypeMapBox.fromJson(json[r'mapTypeMapbox']), mapTypeMapbox: MapTypeMapBox.fromJson(json[r'mapTypeMapbox']),

View File

@ -0,0 +1,91 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class PushNotificationDTO {
PushNotificationDTO({
this.id,
this.instanceId,
this.title,
this.body,
this.topic,
this.status,
this.scheduledAt,
this.sentAt,
this.dateCreation,
});
String? id;
String? instanceId;
String? title;
String? body;
String? topic;
String? status;
DateTime? scheduledAt;
DateTime? sentAt;
DateTime? dateCreation;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PushNotificationDTO && other.id == id;
@override
int get hashCode => (id == null ? 0 : id!.hashCode);
@override
String toString() => 'PushNotificationDTO[id=$id, title=$title, status=$status]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'id'] = id;
json[r'instanceId'] = instanceId;
json[r'title'] = title;
json[r'body'] = body;
json[r'topic'] = topic;
json[r'status'] = status;
if (scheduledAt != null) json[r'scheduledAt'] = scheduledAt!.toUtc().toIso8601String();
if (sentAt != null) json[r'sentAt'] = sentAt!.toUtc().toIso8601String();
if (dateCreation != null) json[r'dateCreation'] = dateCreation!.toUtc().toIso8601String();
return json;
}
static PushNotificationDTO? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return PushNotificationDTO(
id: mapValueOfType<String>(json, r'id'),
instanceId: mapValueOfType<String>(json, r'instanceId'),
title: mapValueOfType<String>(json, r'title'),
body: mapValueOfType<String>(json, r'body'),
topic: mapValueOfType<String>(json, r'topic'),
status: mapValueOfType<String>(json, r'status'),
scheduledAt: mapDateTime(json, r'scheduledAt', r''),
sentAt: mapDateTime(json, r'sentAt', r''),
dateCreation: mapDateTime(json, r'dateCreation', r''),
);
}
return null;
}
static List<PushNotificationDTO> listFromJson(dynamic json, {bool growable = false}) {
final result = <PushNotificationDTO>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PushNotificationDTO.fromJson(row);
if (value != null) result.add(value);
}
}
return result.toList(growable: growable);
}
static const requiredKeys = <String>{};
}

View File

@ -0,0 +1,75 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ScheduleNotificationRequest {
ScheduleNotificationRequest({
this.title,
this.body,
this.scheduledAt,
});
String? title;
String? body;
DateTime? scheduledAt;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ScheduleNotificationRequest &&
other.title == title &&
other.body == body &&
other.scheduledAt == scheduledAt;
@override
int get hashCode =>
(title == null ? 0 : title!.hashCode) +
(body == null ? 0 : body!.hashCode) +
(scheduledAt == null ? 0 : scheduledAt!.hashCode);
@override
String toString() => 'ScheduleNotificationRequest[title=$title, body=$body, scheduledAt=$scheduledAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'title'] = title;
json[r'body'] = body;
if (scheduledAt != null) {
json[r'scheduledAt'] = scheduledAt!.toUtc().toIso8601String();
}
return json;
}
static ScheduleNotificationRequest? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ScheduleNotificationRequest(
title: mapValueOfType<String>(json, r'title'),
body: mapValueOfType<String>(json, r'body'),
scheduledAt: mapDateTime(json, r'scheduledAt', r''),
);
}
return null;
}
static List<ScheduleNotificationRequest> listFromJson(dynamic json, {bool growable = false}) {
final result = <ScheduleNotificationRequest>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ScheduleNotificationRequest.fromJson(row);
if (value != null) result.add(value);
}
}
return result.toList(growable: growable);
}
static const requiredKeys = <String>{};
}

View File

@ -34,7 +34,9 @@ class SectionEventDTO {
this.beaconId, this.beaconId,
this.startDate, this.startDate,
this.endDate, this.endDate,
this.baseSectionMapId,
this.parcoursIds = const [], this.parcoursIds = const [],
this.globalMapAnnotations = const [],
this.programme = const [], this.programme = const [],
}); });
@ -116,8 +118,12 @@ class SectionEventDTO {
/// ///
DateTime? endDate; DateTime? endDate;
String? baseSectionMapId;
List<String>? parcoursIds; List<String>? parcoursIds;
List<MapAnnotationDTO>? globalMapAnnotations;
List<ProgrammeBlock>? programme; List<ProgrammeBlock>? programme;
@override @override
@ -145,7 +151,9 @@ class SectionEventDTO {
other.beaconId == beaconId && other.beaconId == beaconId &&
other.startDate == startDate && other.startDate == startDate &&
other.endDate == endDate && other.endDate == endDate &&
other.baseSectionMapId == baseSectionMapId &&
_deepEquality.equals(other.parcoursIds, parcoursIds) && _deepEquality.equals(other.parcoursIds, parcoursIds) &&
_deepEquality.equals(other.globalMapAnnotations, globalMapAnnotations) &&
_deepEquality.equals(other.programme, programme); _deepEquality.equals(other.programme, programme);
@override @override
@ -172,7 +180,9 @@ class SectionEventDTO {
(beaconId == null ? 0 : beaconId!.hashCode) + (beaconId == null ? 0 : beaconId!.hashCode) +
(startDate == null ? 0 : startDate!.hashCode) + (startDate == null ? 0 : startDate!.hashCode) +
(endDate == null ? 0 : endDate!.hashCode) + (endDate == null ? 0 : endDate!.hashCode) +
(baseSectionMapId == null ? 0 : baseSectionMapId!.hashCode) +
(parcoursIds == null ? 0 : parcoursIds!.hashCode) + (parcoursIds == null ? 0 : parcoursIds!.hashCode) +
(globalMapAnnotations == null ? 0 : globalMapAnnotations!.hashCode) +
(programme == null ? 0 : programme!.hashCode); (programme == null ? 0 : programme!.hashCode);
@override @override
@ -286,11 +296,21 @@ class SectionEventDTO {
} else { } else {
json[r'endDate'] = null; json[r'endDate'] = null;
} }
if (this.baseSectionMapId != null) {
json[r'baseSectionMapId'] = this.baseSectionMapId;
} else {
json[r'baseSectionMapId'] = null;
}
if (this.parcoursIds != null) { if (this.parcoursIds != null) {
json[r'parcoursIds'] = this.parcoursIds; json[r'parcoursIds'] = this.parcoursIds;
} else { } else {
json[r'parcoursIds'] = null; json[r'parcoursIds'] = null;
} }
if (this.globalMapAnnotations != null) {
json[r'globalMapAnnotations'] = this.globalMapAnnotations;
} else {
json[r'globalMapAnnotations'] = null;
}
if (this.programme != null) { if (this.programme != null) {
json[r'programme'] = this.programme; json[r'programme'] = this.programme;
} else { } else {
@ -341,11 +361,13 @@ class SectionEventDTO {
beaconId: mapValueOfType<int>(json, r'beaconId'), beaconId: mapValueOfType<int>(json, r'beaconId'),
startDate: mapDateTime(json, r'startDate', r''), startDate: mapDateTime(json, r'startDate', r''),
endDate: mapDateTime(json, r'endDate', r''), endDate: mapDateTime(json, r'endDate', r''),
baseSectionMapId: mapValueOfType<String>(json, r'baseSectionMapId'),
parcoursIds: json[r'parcoursIds'] is Iterable parcoursIds: json[r'parcoursIds'] is Iterable
? (json[r'parcoursIds'] as Iterable) ? (json[r'parcoursIds'] as Iterable)
.cast<String>() .cast<String>()
.toList(growable: false) .toList(growable: false)
: const [], : const [],
globalMapAnnotations: MapAnnotationDTO.listFromJson(json[r'globalMapAnnotations']),
programme: ProgrammeBlock.listFromJson(json[r'programme']), programme: ProgrammeBlock.listFromJson(json[r'programme']),
); );
} }

View File

@ -0,0 +1,67 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SendNotificationRequest {
SendNotificationRequest({
this.title,
this.body,
});
String? title;
String? body;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SendNotificationRequest &&
other.title == title &&
other.body == body;
@override
int get hashCode =>
(title == null ? 0 : title!.hashCode) +
(body == null ? 0 : body!.hashCode);
@override
String toString() => 'SendNotificationRequest[title=$title, body=$body]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'title'] = title;
json[r'body'] = body;
return json;
}
static SendNotificationRequest? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return SendNotificationRequest(
title: mapValueOfType<String>(json, r'title'),
body: mapValueOfType<String>(json, r'body'),
);
}
return null;
}
static List<SendNotificationRequest> listFromJson(dynamic json, {bool growable = false}) {
final result = <SendNotificationRequest>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SendNotificationRequest.fromJson(row);
if (value != null) result.add(value);
}
}
return result.toList(growable: growable);
}
static const requiredKeys = <String>{};
}

View File

@ -19,7 +19,10 @@ class StatsSummaryDTO {
this.topAgendaEvents = const [], this.topAgendaEvents = const [],
this.quizStats = const [], this.quizStats = const [],
this.gameStats = const [], this.gameStats = const [],
}); this.topArticles = const [],
this.topMenuItems = const [],
QrScanStatDTO? qrScans,
}) : qrScans = qrScans ?? QrScanStatDTO();
int totalSessions; int totalSessions;
int avgVisitDurationSeconds; int avgVisitDurationSeconds;
@ -31,6 +34,9 @@ class StatsSummaryDTO {
List<AgendaEventStatDTO> topAgendaEvents; List<AgendaEventStatDTO> topAgendaEvents;
List<QuizStatDTO> quizStats; List<QuizStatDTO> quizStats;
List<GameStatDTO> gameStats; List<GameStatDTO> gameStats;
List<ArticleStatDTO> topArticles;
List<MenuItemStatDTO> topMenuItems;
QrScanStatDTO qrScans;
static StatsSummaryDTO? fromJson(dynamic value) { static StatsSummaryDTO? fromJson(dynamic value) {
if (value is Map) { if (value is Map) {
@ -50,6 +56,9 @@ class StatsSummaryDTO {
topAgendaEvents: AgendaEventStatDTO.listFromJson(json[r'topAgendaEvents']), topAgendaEvents: AgendaEventStatDTO.listFromJson(json[r'topAgendaEvents']),
quizStats: QuizStatDTO.listFromJson(json[r'quizStats']), quizStats: QuizStatDTO.listFromJson(json[r'quizStats']),
gameStats: GameStatDTO.listFromJson(json[r'gameStats']), gameStats: GameStatDTO.listFromJson(json[r'gameStats']),
topArticles: ArticleStatDTO.listFromJson(json[r'topArticles']),
topMenuItems: MenuItemStatDTO.listFromJson(json[r'topMenuItems']),
qrScans: QrScanStatDTO.fromJson(json[r'qrScans']),
); );
} }
return null; return null;
@ -267,3 +276,83 @@ class GameStatDTO {
return result; return result;
} }
} }
class ArticleStatDTO {
ArticleStatDTO({this.sectionId, this.reads = 0});
String? sectionId;
int reads;
static ArticleStatDTO? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ArticleStatDTO(
sectionId: mapValueOfType<String>(json, r'sectionId'),
reads: mapValueOfType<int>(json, r'reads') ?? 0,
);
}
return null;
}
static List<ArticleStatDTO> listFromJson(dynamic json) {
final result = <ArticleStatDTO>[];
if (json is List) {
for (final row in json) {
final value = ArticleStatDTO.fromJson(row);
if (value != null) result.add(value);
}
}
return result;
}
}
class MenuItemStatDTO {
MenuItemStatDTO({this.targetSectionId, this.menuItemTitle, this.taps = 0});
String? targetSectionId;
String? menuItemTitle;
int taps;
static MenuItemStatDTO? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return MenuItemStatDTO(
targetSectionId: mapValueOfType<String>(json, r'targetSectionId'),
menuItemTitle: mapValueOfType<String>(json, r'menuItemTitle'),
taps: mapValueOfType<int>(json, r'taps') ?? 0,
);
}
return null;
}
static List<MenuItemStatDTO> listFromJson(dynamic json) {
final result = <MenuItemStatDTO>[];
if (json is List) {
for (final row in json) {
final value = MenuItemStatDTO.fromJson(row);
if (value != null) result.add(value);
}
}
return result;
}
}
class QrScanStatDTO {
QrScanStatDTO({this.totalScans = 0, this.validScans = 0, this.invalidScans = 0});
int totalScans;
int validScans;
int invalidScans;
static QrScanStatDTO fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return QrScanStatDTO(
totalScans: mapValueOfType<int>(json, r'totalScans') ?? 0,
validScans: mapValueOfType<int>(json, r'validScans') ?? 0,
invalidScans: mapValueOfType<int>(json, r'invalidScans') ?? 0,
);
}
return QrScanStatDTO();
}
}

View File

@ -232,6 +232,7 @@ class VisitEventType {
static const agendaEventTap = 'AgendaEventTap'; static const agendaEventTap = 'AgendaEventTap';
static const menuItemTap = 'MenuItemTap'; static const menuItemTap = 'MenuItemTap';
static const assistantMessage = 'AssistantMessage'; static const assistantMessage = 'AssistantMessage';
static const articleRead = 'ArticleRead';
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{}; static const requiredKeys = <String>{};