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:
parent
b7bd8588fe
commit
f90f014f36
65
CLAUDE.md
Normal file
65
CLAUDE.md
Normal 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 : 0–550px
|
||||
- Tablet : 550–800px
|
||||
- Desktop : 801–1920px
|
||||
- 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é
|
||||
```
|
||||
@ -1,12 +1,12 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.9.0'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ buildscript {
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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
3
devtools_options.yaml
Normal 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:
|
||||
@ -1,57 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:manager_app/constants.dart';
|
||||
import 'package:manager_app/Components/loader_animated_pieces.dart';
|
||||
|
||||
class CommonLoader extends StatefulWidget {
|
||||
double? iconSize;
|
||||
class CommonLoader extends StatelessWidget {
|
||||
final double? iconSize;
|
||||
|
||||
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();
|
||||
}
|
||||
const CommonLoader({Key? key, this.iconSize}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
_controller!.forward(from: 0.0);
|
||||
_controller!.addListener(() {
|
||||
if (_controller!.isCompleted) {
|
||||
_controller!.reverse();
|
||||
}
|
||||
if(_controller!.isDismissed){
|
||||
_controller!.forward();
|
||||
}
|
||||
});
|
||||
final size = iconSize ?? 60;
|
||||
return Center(
|
||||
child: RotationTransition(
|
||||
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!)*/,
|
||||
),
|
||||
child: LoaderWaveFloat(size: size),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
130
lib/Components/loader_animated_pieces.dart
Normal file
130
lib/Components/loader_animated_pieces.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,8 @@ class ManagerAppContext with ChangeNotifier{
|
||||
bool? isLoading = false;
|
||||
UserRole? role;
|
||||
|
||||
bool get canEdit => role != null && role!.value <= 2;
|
||||
|
||||
ManagerAppContext({this.email, this.accessToken});
|
||||
|
||||
// Implement toString to make it easier to see information about
|
||||
|
||||
@ -19,8 +19,9 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
||||
List<Map<String, dynamic>> _keys = [];
|
||||
bool _loading = true;
|
||||
|
||||
static String _appTypeName(int? v) {
|
||||
switch (v) {
|
||||
static String _appTypeName(dynamic v) {
|
||||
if (v is String) return v;
|
||||
switch (v as int?) {
|
||||
case 0: return 'VisitApp';
|
||||
case 1: return 'TabletApp';
|
||||
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(
|
||||
CreateApiKeyRequest(name: name, appType: appType),
|
||||
CreateApiKeyRequest(name: name, appType: appType, dateExpiration: dateExpiration),
|
||||
);
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
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) {
|
||||
final nameCtrl = TextEditingController();
|
||||
ApiKeyAppType selectedType = ApiKeyAppType.number0;
|
||||
DateTime? selectedExpiration;
|
||||
|
||||
showDialog(
|
||||
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: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx2), child: const Text('Annuler')),
|
||||
TextButton(onPressed: () => Navigator.of(ctx2, rootNavigator: true).pop(), child: const Text('Annuler')),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(ctx2);
|
||||
final plainKey = await _createKey(ctx, nameCtrl.text, selectedType);
|
||||
Navigator.of(ctx2, rootNavigator: true).pop();
|
||||
final plainKey = await _createKey(ctx, nameCtrl.text, selectedType, selectedExpiration);
|
||||
if (plainKey != null && context.mounted) {
|
||||
_showPlainKeyDialog(context, plainKey);
|
||||
}
|
||||
@ -135,7 +168,7 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
||||
]),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => Navigator.of(context, rootNavigator: true).pop(),
|
||||
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) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Révoquer la clé API'),
|
||||
content: Text('Révoquer « ${key['name']} » ? Les apps utilisant cette clé perdront l\'accès.'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Annuler')),
|
||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Annuler')),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(dialogContext);
|
||||
await _revokeKey(ctx, key['id'] as String);
|
||||
},
|
||||
child: const Text('Révoquer', style: TextStyle(color: Colors.white)),
|
||||
@ -199,47 +232,75 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
||||
const Center(child: Text('Aucune clé API'))
|
||||
else
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: DataTable(
|
||||
columns: const [
|
||||
DataColumn(label: Text('Nom')),
|
||||
DataColumn(label: Text('Type')),
|
||||
DataColumn(label: Text('Créée le')),
|
||||
DataColumn(label: Text('Statut')),
|
||||
DataColumn(label: Text('Actions')),
|
||||
],
|
||||
rows: _keys.map((key) {
|
||||
final isActive = key['isActive'] as bool? ?? false;
|
||||
final dateRaw = key['dateCreation'] as String?;
|
||||
final date = dateRaw != null
|
||||
? DateTime.tryParse(dateRaw)?.toLocal()
|
||||
: null;
|
||||
final dateStr = date != null
|
||||
? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'
|
||||
: '—';
|
||||
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: SizedBox(
|
||||
width: double.infinity,
|
||||
child: DataTable(
|
||||
horizontalMargin: 16,
|
||||
columnSpacing: 24,
|
||||
headingRowColor: WidgetStateProperty.all(Colors.grey.shade50),
|
||||
dividerThickness: 1,
|
||||
columns: const [
|
||||
DataColumn(label: Text('Nom', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
DataColumn(label: Text('Type', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
DataColumn(label: Text('Créée le', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
DataColumn(label: Text('Expiration', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
DataColumn(label: Text('Statut', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
DataColumn(label: Text('Actions', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
],
|
||||
rows: _keys.map((key) {
|
||||
final isActive = key['isActive'] as bool? ?? false;
|
||||
final dateRaw = key['dateCreation'] as String?;
|
||||
final date = dateRaw != null ? DateTime.tryParse(dateRaw)?.toLocal() : null;
|
||||
final dateStr = date != null
|
||||
? '${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: [
|
||||
DataCell(Text(key['name'] as String? ?? '')),
|
||||
DataCell(Text(_appTypeName(key['appType'] as int?))),
|
||||
DataCell(Text(dateStr)),
|
||||
DataCell(Chip(
|
||||
label: Text(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,
|
||||
side: BorderSide(color: isActive ? Colors.green.shade200 : Colors.red.shade200),
|
||||
)),
|
||||
DataCell(isActive
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.block, color: Colors.red),
|
||||
tooltip: 'Révoquer',
|
||||
onPressed: () => _confirmRevoke(context, managerCtx, key),
|
||||
)
|
||||
: const SizedBox()),
|
||||
]);
|
||||
}).toList(),
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(key['name'] as String? ?? '')),
|
||||
DataCell(Text(_appTypeName(key['appType']))),
|
||||
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(
|
||||
label: Text(
|
||||
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,
|
||||
side: BorderSide(color: isActive ? Colors.green.shade200 : Colors.red.shade200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
visualDensity: VisualDensity.compact,
|
||||
)),
|
||||
DataCell(isActive
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.block, color: Colors.red, size: 20),
|
||||
tooltip: 'Révoquer',
|
||||
onPressed: () => _confirmRevoke(context, managerCtx, key),
|
||||
)
|
||||
: const SizedBox()),
|
||||
]);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ class _AgendaConfigState extends State<AgendaConfig> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildAgendaHeader(size, mapProviderIn),
|
||||
if (agendaDTO.isOnlineAgenda == false) ...[
|
||||
...[
|
||||
const Divider(height: 32),
|
||||
Padding(
|
||||
padding:
|
||||
@ -94,7 +94,7 @@ class _AgendaConfigState extends State<AgendaConfig> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Évènements Manuels",
|
||||
const Text("Évènements",
|
||||
style:
|
||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
ElevatedButton.icon(
|
||||
@ -146,7 +146,7 @@ class _AgendaConfigState extends State<AgendaConfig> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: events.isEmpty
|
||||
? const Center(
|
||||
child: Text("Aucun évènement manuel",
|
||||
child: Text("Aucun évènement",
|
||||
style: TextStyle(fontStyle: FontStyle.italic)))
|
||||
: ListView.builder(
|
||||
itemCount: events.length,
|
||||
@ -288,25 +288,40 @@ class _AgendaConfigState extends State<AgendaConfig> {
|
||||
});
|
||||
},
|
||||
),
|
||||
SingleSelectContainer(
|
||||
label: "Service carte :",
|
||||
color: Colors.black,
|
||||
initialValue: mapProviderIn,
|
||||
inputValues: const ["Google", "MapBox"],
|
||||
onChanged: (String value) {
|
||||
CheckInputContainer(
|
||||
label: "Vue carte :",
|
||||
isChecked: agendaDTO.agendaMapProvider != null,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
switch (value) {
|
||||
case "Google":
|
||||
agendaDTO.agendaMapProvider = MapProvider.Google;
|
||||
break;
|
||||
case "MapBox":
|
||||
agendaDTO.agendaMapProvider = MapProvider.MapBox;
|
||||
break;
|
||||
if (value) {
|
||||
agendaDTO.agendaMapProvider = MapProvider.Google;
|
||||
} else {
|
||||
agendaDTO.agendaMapProvider = null;
|
||||
}
|
||||
widget.onChanged(agendaDTO);
|
||||
});
|
||||
},
|
||||
),
|
||||
if (agendaDTO.agendaMapProvider != null)
|
||||
SingleSelectContainer(
|
||||
label: "Service carte :",
|
||||
color: Colors.black,
|
||||
initialValue: mapProviderIn,
|
||||
inputValues: const ["Google", "MapBox"],
|
||||
onChanged: (String value) {
|
||||
setState(() {
|
||||
switch (value) {
|
||||
case "Google":
|
||||
agendaDTO.agendaMapProvider = MapProvider.Google;
|
||||
break;
|
||||
case "MapBox":
|
||||
agendaDTO.agendaMapProvider = MapProvider.MapBox;
|
||||
break;
|
||||
}
|
||||
widget.onChanged(agendaDTO);
|
||||
});
|
||||
},
|
||||
),
|
||||
if (agendaDTO.isOnlineAgenda == true)
|
||||
MultiStringInputContainer(
|
||||
label: "Fichiers json :",
|
||||
|
||||
@ -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/string_input_container.dart';
|
||||
import 'package:manager_app/Components/geometry_input_container.dart';
|
||||
import 'package:manager_app/Components/resource_input_container.dart';
|
||||
|
||||
void showNewOrUpdateEventAgenda(
|
||||
BuildContext context,
|
||||
@ -95,6 +96,117 @@ void showNewOrUpdateEventAgenda(
|
||||
],
|
||||
),
|
||||
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
|
||||
Row(
|
||||
children: [
|
||||
|
||||
@ -8,6 +8,9 @@ import 'package:provider/provider.dart';
|
||||
import 'package:manager_app/app_context.dart';
|
||||
import 'package:manager_app/Models/managerContext.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 {
|
||||
final SectionEventDTO initialValue;
|
||||
@ -25,12 +28,17 @@ class EventConfig extends StatefulWidget {
|
||||
|
||||
class _EventConfigState extends State<EventConfig> {
|
||||
late SectionEventDTO eventDTO;
|
||||
List<SectionDTO> availableMaps = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
eventDTO = widget.initialValue;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _loadProgrammeBlocks());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadProgrammeBlocks();
|
||||
_loadGlobalAnnotations();
|
||||
_loadAvailableMaps();
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@ -182,6 +220,12 @@ class _EventConfigState extends State<EventConfig> {
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
// --- Carte de base ---
|
||||
_buildBaseSectionMapSection(),
|
||||
Divider(),
|
||||
// --- Annotations globales ---
|
||||
_buildGlobalAnnotationsSection(),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api_new/api.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/multi_string_input_container.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'showNewOrUpdateMapAnnotation.dart';
|
||||
|
||||
void showNewOrUpdateProgrammeBlock(
|
||||
BuildContext context,
|
||||
@ -17,14 +19,21 @@ void showNewOrUpdateProgrammeBlock(
|
||||
description: List<TranslationDTO>.from(block.description ?? []),
|
||||
startTime: block.startTime,
|
||||
endTime: block.endTime,
|
||||
mapAnnotations: List<MapAnnotation>.from(block.mapAnnotations ?? []),
|
||||
)
|
||||
: ProgrammeBlock(
|
||||
title: [],
|
||||
description: [],
|
||||
startTime: DateTime.now(),
|
||||
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(
|
||||
context: 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -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/multi_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/string_input_container.dart';
|
||||
import 'package:manager_app/Screens/Configurations/Section/SubSection/Map/showNewOrUpdateGeoPoint.dart';
|
||||
@ -662,6 +663,16 @@ class _MapConfigState extends State<MapConfig> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
CheckInputContainer(
|
||||
label: "Vue liste :",
|
||||
isChecked: mapDTO.isListViewEnabled ?? false,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
mapDTO.isListViewEnabled = value;
|
||||
widget.onChanged(mapDTO);
|
||||
});
|
||||
},
|
||||
),
|
||||
if (mapDTO.mapProvider == MapProvider.Google)
|
||||
DropDownInputContainer(
|
||||
label: "Type :",
|
||||
|
||||
@ -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/multi_string_input_container.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 '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
|
||||
if (isEscapeMode) ...[
|
||||
Divider(height: 24),
|
||||
|
||||
@ -391,6 +391,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
||||
}
|
||||
|
||||
getButtons(SectionDTO sectionDTO, AppContext appContext) {
|
||||
final canEdit = (appContext.getContext() as ManagerAppContext).canEdit;
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: Row(
|
||||
@ -409,7 +410,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
if (canEdit) Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RoundedButton(
|
||||
text: "Supprimer",
|
||||
@ -422,7 +423,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
if (canEdit) Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RoundedButton(
|
||||
text: "Sauvegarder",
|
||||
|
||||
@ -346,6 +346,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
||||
}
|
||||
|
||||
getButtons(ConfigurationDTO configurationDTO, AppContext appContext) {
|
||||
final canEdit = (appContext.getContext() as ManagerAppContext).canEdit;
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: Row(
|
||||
@ -364,7 +365,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
if (canEdit) Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: RoundedButton(
|
||||
text: "Supprimer",
|
||||
@ -377,7 +378,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
if (canEdit) Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: RoundedButton(
|
||||
text: "Sauvegarder",
|
||||
|
||||
@ -43,7 +43,7 @@ class _ConfigurationsScreenState extends State<ConfigurationsScreen> {
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
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);
|
||||
} else if (snapshot.connectionState == ConnectionState.none) {
|
||||
return Text("No data");
|
||||
|
||||
@ -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/Statistics/statistics_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/app_context.dart';
|
||||
import 'package:manager_app/constants.dart';
|
||||
@ -40,6 +41,11 @@ class _MainScreenState extends State<MainScreen> {
|
||||
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -115,7 +121,7 @@ class _MainScreenState extends State<MainScreen> {
|
||||
onTap: isCurrent
|
||||
? null
|
||||
: () async {
|
||||
Navigator.pop(context);
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
final newInstance = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id);
|
||||
if (newInstance != null && context.mounted) {
|
||||
setState(() {
|
||||
@ -135,12 +141,25 @@ class _MainScreenState extends State<MainScreen> {
|
||||
),
|
||||
),
|
||||
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) {
|
||||
return Container(
|
||||
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) {
|
||||
return Container(
|
||||
key: ValueKey(section.type),
|
||||
decoration: currentPath!.contains(section.type)
|
||||
? BoxDecoration(
|
||||
border: Border(
|
||||
@ -201,21 +221,38 @@ class _MainScreenState extends State<MainScreen> {
|
||||
)
|
||||
: null,
|
||||
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)),
|
||||
selected: currentPosition == section.menuId,
|
||||
onTap: () {
|
||||
//currentPosition.value = section.menuId;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
||||
_nudgeKey(section.type).currentState?.nudge());
|
||||
context.go('/main/${section.type}');
|
||||
if (isDrawer) Navigator.of(context).pop(); // Close drawer on mobile
|
||||
if (isDrawer) Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final isAppActive = currentPath!.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr");
|
||||
return Container(
|
||||
key: ValueKey(section.type),
|
||||
child: ExpansionTile(
|
||||
iconColor: currentPath!.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? kPrimaryColor : kBodyTextColor,
|
||||
collapsedIconColor: currentPath.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? kPrimaryColor : kBodyTextColor,
|
||||
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)),
|
||||
leading: _NudgeIcon(
|
||||
key: _nudgeKey(section.type),
|
||||
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) {
|
||||
return Container(
|
||||
decoration: currentPath.contains(subSection.type)
|
||||
@ -246,11 +283,11 @@ class _MainScreenState extends State<MainScreen> {
|
||||
),
|
||||
selected: currentPosition.value == subSection.menuId,
|
||||
onTap: () {
|
||||
|
||||
|
||||
if(currentPath != null && currentPath.contains(subSection.type)) {
|
||||
// DO NOTHING, we are already display the correct interface
|
||||
if (currentPath != null && currentPath.contains(subSection.type)) {
|
||||
// already on this page
|
||||
} else {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
||||
_nudgeKey(section.type).currentState?.nudge());
|
||||
context.go('/main/${subSection.type}');
|
||||
}
|
||||
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
|
||||
final role = managerAppContext.role;
|
||||
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) {
|
||||
menu.sections!.add(MenuSection(name: "Utilisateurs", type: "users", menuId: 8, 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":
|
||||
currentPosition = 9;
|
||||
break;
|
||||
case "notifications":
|
||||
currentPosition = 10;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -473,7 +519,59 @@ class _MainScreenState extends State<MainScreen> {
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: ApiKeysScreen()
|
||||
);
|
||||
case 'notifications':
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: NotificationsScreen()
|
||||
);
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
454
lib/Screens/Notifications/notifications_screen.dart
Normal file
454
lib/Screens/Notifications/notifications_screen.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:manager_app/Components/fetch_resource_icon.dart';
|
||||
import 'package:manager_app/Components/multi_select_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/constants.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(
|
||||
onTap: () {
|
||||
widget.onSelect(ResourceDTO(id: widget.isSelectModal ? "-1" : null));
|
||||
|
||||
@ -17,14 +17,23 @@ class _UsersScreenState extends State<UsersScreen> {
|
||||
List<Map<String, dynamic>> _users = [];
|
||||
bool _loading = true;
|
||||
|
||||
static String _roleName(int? v) {
|
||||
switch (v) {
|
||||
case 0: return 'SuperAdmin';
|
||||
case 1: return 'InstanceAdmin';
|
||||
case 2: return 'ContentEditor';
|
||||
case 3: return 'Viewer';
|
||||
default: return '—';
|
||||
static const _roleNames = ['SuperAdmin', 'InstanceAdmin', 'ContentEditor', 'Viewer'];
|
||||
|
||||
static String _roleName(dynamic v) {
|
||||
if (v is String) return v;
|
||||
final i = v as int?;
|
||||
if (i != null && i >= 0 && i < _roleNames.length) return _roleNames[i];
|
||||
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)
|
||||
@ -79,7 +88,7 @@ class _UsersScreenState extends State<UsersScreen> {
|
||||
}
|
||||
|
||||
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
|
||||
final callerRole = ctx.role?.value ?? 2;
|
||||
final callerRole = _roleToInt(ctx.role?.value);
|
||||
final emailCtrl = TextEditingController();
|
||||
final firstCtrl = TextEditingController();
|
||||
final lastCtrl = TextEditingController();
|
||||
@ -131,10 +140,10 @@ class _UsersScreenState extends State<UsersScreen> {
|
||||
}
|
||||
|
||||
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 lastCtrl = TextEditingController(text: user['lastName'] as String? ?? '');
|
||||
int selectedRole = (user['role'] as int?) ?? callerRole;
|
||||
int selectedRole = _roleToInt(user['role']);
|
||||
|
||||
showDialog(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
@ -234,36 +252,64 @@ class _UsersScreenState extends State<UsersScreen> {
|
||||
const Center(child: Text('Aucun utilisateur'))
|
||||
else
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: DataTable(
|
||||
columns: const [
|
||||
DataColumn(label: Text('Email')),
|
||||
DataColumn(label: Text('Prénom')),
|
||||
DataColumn(label: Text('Nom')),
|
||||
DataColumn(label: Text('Rôle')),
|
||||
DataColumn(label: Text('Actions')),
|
||||
],
|
||||
rows: _users.map((user) {
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(user['email'] as String? ?? '')),
|
||||
DataCell(Text(user['firstName'] as String? ?? '')),
|
||||
DataCell(Text(user['lastName'] as String? ?? '')),
|
||||
DataCell(Text(_roleName(user['role'] as int?))),
|
||||
DataCell(Row(children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit, color: kPrimaryColor),
|
||||
onPressed: () => _showEditDialog(context, managerCtx, user),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => _confirmDelete(context, managerCtx, user),
|
||||
),
|
||||
])),
|
||||
]);
|
||||
}).toList(),
|
||||
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: SizedBox(
|
||||
width: double.infinity,
|
||||
child: DataTable(
|
||||
horizontalMargin: 16,
|
||||
columnSpacing: 24,
|
||||
headingRowColor: WidgetStateProperty.all(Colors.grey.shade50),
|
||||
dividerThickness: 1,
|
||||
columns: const [
|
||||
DataColumn(label: Text('Email', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
DataColumn(label: Text('Prénom', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
DataColumn(label: Text('Nom', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
DataColumn(label: Text('Rôle', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
DataColumn(label: Text('Actions', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||
],
|
||||
rows: _users.map((user) {
|
||||
final roleColor = _roleColor(user['role']);
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(user['email'] as String? ?? '')),
|
||||
DataCell(Text(user['firstName'] as String? ?? '')),
|
||||
DataCell(Text(user['lastName'] as String? ?? '')),
|
||||
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: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit, color: kPrimaryColor, size: 20),
|
||||
tooltip: 'Modifier',
|
||||
onPressed: () => _showEditDialog(context, managerCtx, user),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20),
|
||||
tooltip: 'Supprimer',
|
||||
onPressed: () => _confirmDelete(context, managerCtx, user),
|
||||
),
|
||||
])),
|
||||
]);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -47,6 +47,9 @@ class Client {
|
||||
ApiKeyApi? _apiKeyApi;
|
||||
ApiKeyApi? get apiKeyApi => _apiKeyApi;
|
||||
|
||||
NotificationApi? _notificationApi;
|
||||
NotificationApi? get notificationApi => _notificationApi;
|
||||
|
||||
Client(String path) {
|
||||
_apiClient = ApiClient(basePath: path);
|
||||
//basePath: "https://192.168.31.140");
|
||||
@ -66,5 +69,6 @@ class Client {
|
||||
_sectionEventApi = SectionEventApi(_apiClient);
|
||||
_statsApi = StatsApi(_apiClient);
|
||||
_apiKeyApi = ApiKeyApi(_apiClient);
|
||||
_notificationApi = NotificationApi(_apiClient);
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,10 +263,15 @@ class _MyAppState extends State<MyApp> {
|
||||
//const Locale('fr', 'FR'),
|
||||
],*/
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
primaryColor: kPrimaryColor,
|
||||
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)),
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
|
||||
@ -41,9 +41,14 @@ part 'api/section_agenda_api.dart';
|
||||
part 'api/section_event_api.dart';
|
||||
part 'api/section_map_api.dart';
|
||||
part 'api/section_quiz_api.dart';
|
||||
part 'api/notification_api.dart';
|
||||
part 'api/stats_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_all_of_agenda_map_provider.dart';
|
||||
part 'model/ai_card_dto.dart';
|
||||
|
||||
97
manager_api_new/lib/api/notification_api.dart
Normal file
97
manager_api_new/lib/api/notification_api.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -193,6 +193,54 @@ class SectionAgendaApi {
|
||||
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].
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
@ -662,7 +662,7 @@ class SectionApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<List<Object>?> sectionGetFromConfigurationDetail(
|
||||
Future<dynamic> sectionGetFromConfigurationDetail(
|
||||
String id,
|
||||
) async {
|
||||
final response = await sectionGetFromConfigurationDetailWithHttpInfo(
|
||||
@ -671,16 +671,10 @@ class SectionApi {
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
// Sections are polymorphic — bypass generated deserialization and return raw JSON
|
||||
if (response.body.isNotEmpty &&
|
||||
response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<Object>')
|
||||
as List)
|
||||
.cast<Object>()
|
||||
.toList(growable: false);
|
||||
return json.decode(await _decodeBodyBytes(response));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -481,4 +481,91 @@ class SectionEventApi {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ class AiChatResponseNavigation {
|
||||
this.sectionId,
|
||||
this.sectionTitle,
|
||||
this.sectionType,
|
||||
this.imageUrl,
|
||||
});
|
||||
|
||||
String? sectionId;
|
||||
@ -24,6 +25,8 @@ class AiChatResponseNavigation {
|
||||
|
||||
String? sectionType;
|
||||
|
||||
String? imageUrl;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
@ -60,6 +63,11 @@ class AiChatResponseNavigation {
|
||||
} else {
|
||||
json[r'sectionType'] = null;
|
||||
}
|
||||
if (this.imageUrl != null) {
|
||||
json[r'imageUrl'] = this.imageUrl;
|
||||
} else {
|
||||
json[r'imageUrl'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -87,6 +95,7 @@ class AiChatResponseNavigation {
|
||||
sectionId: mapValueOfType<String>(json, r'sectionId'),
|
||||
sectionTitle: mapValueOfType<String>(json, r'sectionTitle'),
|
||||
sectionType: mapValueOfType<String>(json, r'sectionType'),
|
||||
imageUrl: mapValueOfType<String>(json, r'imageUrl'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@ -15,6 +15,7 @@ class CreateApiKeyRequest {
|
||||
CreateApiKeyRequest({
|
||||
this.name,
|
||||
this.appType,
|
||||
this.dateExpiration,
|
||||
});
|
||||
|
||||
String? name;
|
||||
@ -27,21 +28,25 @@ class CreateApiKeyRequest {
|
||||
///
|
||||
ApiKeyAppType? appType;
|
||||
|
||||
DateTime? dateExpiration;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CreateApiKeyRequest &&
|
||||
other.name == name &&
|
||||
other.appType == appType;
|
||||
other.appType == appType &&
|
||||
other.dateExpiration == dateExpiration;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(appType == null ? 0 : appType!.hashCode);
|
||||
(appType == null ? 0 : appType!.hashCode) +
|
||||
(dateExpiration == null ? 0 : dateExpiration!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CreateApiKeyRequest[name=$name, appType=$appType]';
|
||||
String toString() => 'CreateApiKeyRequest[name=$name, appType=$appType, dateExpiration=$dateExpiration]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -55,6 +60,11 @@ class CreateApiKeyRequest {
|
||||
} else {
|
||||
json[r'appType'] = null;
|
||||
}
|
||||
if (this.dateExpiration != null) {
|
||||
json[r'dateExpiration'] = this.dateExpiration!.toUtc().toIso8601String();
|
||||
} else {
|
||||
json[r'dateExpiration'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -81,6 +91,7 @@ class CreateApiKeyRequest {
|
||||
return CreateApiKeyRequest(
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
appType: ApiKeyAppType.fromJson(json[r'appType']),
|
||||
dateExpiration: mapDateTime(json, r'dateExpiration', r''),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@ -28,6 +28,11 @@ class EventAgendaDTO {
|
||||
this.email,
|
||||
this.sectionAgendaId,
|
||||
this.sectionEventId,
|
||||
this.isSynced,
|
||||
this.idVideoYoutube,
|
||||
this.videoLink,
|
||||
this.videoResourceId,
|
||||
this.videoResource,
|
||||
});
|
||||
|
||||
///
|
||||
@ -66,6 +71,16 @@ class EventAgendaDTO {
|
||||
|
||||
String? sectionEventId;
|
||||
|
||||
bool? isSynced;
|
||||
|
||||
String? idVideoYoutube;
|
||||
|
||||
String? videoLink;
|
||||
|
||||
String? videoResourceId;
|
||||
|
||||
EventAgendaDTOResource? videoResource;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
@ -84,7 +99,12 @@ class EventAgendaDTO {
|
||||
other.phone == phone &&
|
||||
other.email == email &&
|
||||
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
|
||||
int get hashCode =>
|
||||
@ -103,7 +123,12 @@ class EventAgendaDTO {
|
||||
(phone == null ? 0 : phone!.hashCode) +
|
||||
(email == null ? 0 : email!.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
|
||||
String toString() =>
|
||||
@ -186,6 +211,31 @@ class EventAgendaDTO {
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -225,6 +275,11 @@ class EventAgendaDTO {
|
||||
email: mapValueOfType<String>(json, r'email'),
|
||||
sectionAgendaId: mapValueOfType<String>(json, r'sectionAgendaId'),
|
||||
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;
|
||||
|
||||
@ -32,6 +32,7 @@ class MapDTO {
|
||||
this.meterZoneGPS,
|
||||
this.isBeacon,
|
||||
this.beaconId,
|
||||
this.isListViewEnabled,
|
||||
this.zoom,
|
||||
this.mapType,
|
||||
this.mapTypeMapbox,
|
||||
@ -114,6 +115,8 @@ class MapDTO {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isListViewEnabled;
|
||||
|
||||
int? zoom;
|
||||
|
||||
MapTypeApp? mapType;
|
||||
@ -161,6 +164,7 @@ class MapDTO {
|
||||
other.meterZoneGPS == meterZoneGPS &&
|
||||
other.isBeacon == isBeacon &&
|
||||
other.beaconId == beaconId &&
|
||||
other.isListViewEnabled == isListViewEnabled &&
|
||||
other.zoom == zoom &&
|
||||
other.mapType == mapType &&
|
||||
other.mapTypeMapbox == mapTypeMapbox &&
|
||||
@ -196,6 +200,7 @@ class MapDTO {
|
||||
(meterZoneGPS == null ? 0 : meterZoneGPS!.hashCode) +
|
||||
(isBeacon == null ? 0 : isBeacon!.hashCode) +
|
||||
(beaconId == null ? 0 : beaconId!.hashCode) +
|
||||
(isListViewEnabled == null ? 0 : isListViewEnabled!.hashCode) +
|
||||
(zoom == null ? 0 : zoom!.hashCode) +
|
||||
(mapType == null ? 0 : mapType!.hashCode) +
|
||||
(mapTypeMapbox == null ? 0 : mapTypeMapbox!.hashCode) +
|
||||
@ -310,6 +315,11 @@ class MapDTO {
|
||||
} else {
|
||||
json[r'beaconId'] = null;
|
||||
}
|
||||
if (this.isListViewEnabled != null) {
|
||||
json[r'isListViewEnabled'] = this.isListViewEnabled;
|
||||
} else {
|
||||
json[r'isListViewEnabled'] = null;
|
||||
}
|
||||
if (this.zoom != null) {
|
||||
json[r'zoom'] = this.zoom;
|
||||
} else {
|
||||
@ -413,6 +423,7 @@ class MapDTO {
|
||||
meterZoneGPS: mapValueOfType<int>(json, r'meterZoneGPS'),
|
||||
isBeacon: mapValueOfType<bool>(json, r'isBeacon'),
|
||||
beaconId: mapValueOfType<int>(json, r'beaconId'),
|
||||
isListViewEnabled: mapValueOfType<bool>(json, r'isListViewEnabled'),
|
||||
zoom: mapValueOfType<int>(json, r'zoom'),
|
||||
mapType: MapTypeApp.fromJson(json[r'mapType']),
|
||||
mapTypeMapbox: MapTypeMapBox.fromJson(json[r'mapTypeMapbox']),
|
||||
|
||||
91
manager_api_new/lib/model/push_notification_dto.dart
Normal file
91
manager_api_new/lib/model/push_notification_dto.dart
Normal 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>{};
|
||||
}
|
||||
75
manager_api_new/lib/model/schedule_notification_request.dart
Normal file
75
manager_api_new/lib/model/schedule_notification_request.dart
Normal 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>{};
|
||||
}
|
||||
@ -34,7 +34,9 @@ class SectionEventDTO {
|
||||
this.beaconId,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.baseSectionMapId,
|
||||
this.parcoursIds = const [],
|
||||
this.globalMapAnnotations = const [],
|
||||
this.programme = const [],
|
||||
});
|
||||
|
||||
@ -116,8 +118,12 @@ class SectionEventDTO {
|
||||
///
|
||||
DateTime? endDate;
|
||||
|
||||
String? baseSectionMapId;
|
||||
|
||||
List<String>? parcoursIds;
|
||||
|
||||
List<MapAnnotationDTO>? globalMapAnnotations;
|
||||
|
||||
List<ProgrammeBlock>? programme;
|
||||
|
||||
@override
|
||||
@ -145,7 +151,9 @@ class SectionEventDTO {
|
||||
other.beaconId == beaconId &&
|
||||
other.startDate == startDate &&
|
||||
other.endDate == endDate &&
|
||||
other.baseSectionMapId == baseSectionMapId &&
|
||||
_deepEquality.equals(other.parcoursIds, parcoursIds) &&
|
||||
_deepEquality.equals(other.globalMapAnnotations, globalMapAnnotations) &&
|
||||
_deepEquality.equals(other.programme, programme);
|
||||
|
||||
@override
|
||||
@ -172,7 +180,9 @@ class SectionEventDTO {
|
||||
(beaconId == null ? 0 : beaconId!.hashCode) +
|
||||
(startDate == null ? 0 : startDate!.hashCode) +
|
||||
(endDate == null ? 0 : endDate!.hashCode) +
|
||||
(baseSectionMapId == null ? 0 : baseSectionMapId!.hashCode) +
|
||||
(parcoursIds == null ? 0 : parcoursIds!.hashCode) +
|
||||
(globalMapAnnotations == null ? 0 : globalMapAnnotations!.hashCode) +
|
||||
(programme == null ? 0 : programme!.hashCode);
|
||||
|
||||
@override
|
||||
@ -286,11 +296,21 @@ class SectionEventDTO {
|
||||
} else {
|
||||
json[r'endDate'] = null;
|
||||
}
|
||||
if (this.baseSectionMapId != null) {
|
||||
json[r'baseSectionMapId'] = this.baseSectionMapId;
|
||||
} else {
|
||||
json[r'baseSectionMapId'] = null;
|
||||
}
|
||||
if (this.parcoursIds != null) {
|
||||
json[r'parcoursIds'] = this.parcoursIds;
|
||||
} else {
|
||||
json[r'parcoursIds'] = null;
|
||||
}
|
||||
if (this.globalMapAnnotations != null) {
|
||||
json[r'globalMapAnnotations'] = this.globalMapAnnotations;
|
||||
} else {
|
||||
json[r'globalMapAnnotations'] = null;
|
||||
}
|
||||
if (this.programme != null) {
|
||||
json[r'programme'] = this.programme;
|
||||
} else {
|
||||
@ -341,11 +361,13 @@ class SectionEventDTO {
|
||||
beaconId: mapValueOfType<int>(json, r'beaconId'),
|
||||
startDate: mapDateTime(json, r'startDate', r''),
|
||||
endDate: mapDateTime(json, r'endDate', r''),
|
||||
baseSectionMapId: mapValueOfType<String>(json, r'baseSectionMapId'),
|
||||
parcoursIds: json[r'parcoursIds'] is Iterable
|
||||
? (json[r'parcoursIds'] as Iterable)
|
||||
.cast<String>()
|
||||
.toList(growable: false)
|
||||
: const [],
|
||||
globalMapAnnotations: MapAnnotationDTO.listFromJson(json[r'globalMapAnnotations']),
|
||||
programme: ProgrammeBlock.listFromJson(json[r'programme']),
|
||||
);
|
||||
}
|
||||
|
||||
67
manager_api_new/lib/model/send_notification_request.dart
Normal file
67
manager_api_new/lib/model/send_notification_request.dart
Normal 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>{};
|
||||
}
|
||||
@ -19,7 +19,10 @@ class StatsSummaryDTO {
|
||||
this.topAgendaEvents = const [],
|
||||
this.quizStats = const [],
|
||||
this.gameStats = const [],
|
||||
});
|
||||
this.topArticles = const [],
|
||||
this.topMenuItems = const [],
|
||||
QrScanStatDTO? qrScans,
|
||||
}) : qrScans = qrScans ?? QrScanStatDTO();
|
||||
|
||||
int totalSessions;
|
||||
int avgVisitDurationSeconds;
|
||||
@ -31,6 +34,9 @@ class StatsSummaryDTO {
|
||||
List<AgendaEventStatDTO> topAgendaEvents;
|
||||
List<QuizStatDTO> quizStats;
|
||||
List<GameStatDTO> gameStats;
|
||||
List<ArticleStatDTO> topArticles;
|
||||
List<MenuItemStatDTO> topMenuItems;
|
||||
QrScanStatDTO qrScans;
|
||||
|
||||
static StatsSummaryDTO? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
@ -50,6 +56,9 @@ class StatsSummaryDTO {
|
||||
topAgendaEvents: AgendaEventStatDTO.listFromJson(json[r'topAgendaEvents']),
|
||||
quizStats: QuizStatDTO.listFromJson(json[r'quizStats']),
|
||||
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;
|
||||
@ -267,3 +276,83 @@ class GameStatDTO {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,6 +232,7 @@ class VisitEventType {
|
||||
static const agendaEventTap = 'AgendaEventTap';
|
||||
static const menuItemTap = 'MenuItemTap';
|
||||
static const assistantMessage = 'AssistantMessage';
|
||||
static const articleRead = 'ArticleRead';
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user