Compare commits
2 Commits
b7bd8588fe
...
634fd8d4d7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
634fd8d4d7 | ||
|
|
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 {
|
buildscript {
|
||||||
ext.kotlin_version = '1.7.10'
|
ext.kotlin_version = '1.9.0'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.0.1'
|
classpath 'com.android.tools.build:gradle:8.1.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -14,7 +14,7 @@ buildscript {
|
|||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||||
|
|||||||
3
devtools_options.yaml
Normal file
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/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:manager_app/Components/loader_animated_pieces.dart';
|
||||||
import 'package:manager_app/constants.dart';
|
|
||||||
|
|
||||||
class CommonLoader extends StatefulWidget {
|
class CommonLoader extends StatelessWidget {
|
||||||
double? iconSize;
|
final double? iconSize;
|
||||||
|
|
||||||
CommonLoader({Key? key, this.iconSize}) : super(key: key);
|
const CommonLoader({Key? key, this.iconSize}) : super(key: key);
|
||||||
|
|
||||||
@override
|
|
||||||
State<CommonLoader> createState() => _CommonLoaderState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CommonLoaderState extends State<CommonLoader> with TickerProviderStateMixin {
|
|
||||||
AnimationController? _controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_controller = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 5000),
|
|
||||||
vsync: this,
|
|
||||||
)..repeat();
|
|
||||||
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller!.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Size size = MediaQuery.of(context).size;
|
final size = iconSize ?? 60;
|
||||||
_controller!.forward(from: 0.0);
|
|
||||||
_controller!.addListener(() {
|
|
||||||
if (_controller!.isCompleted) {
|
|
||||||
_controller!.reverse();
|
|
||||||
}
|
|
||||||
if(_controller!.isDismissed){
|
|
||||||
_controller!.forward();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Center(
|
return Center(
|
||||||
child: RotationTransition(
|
child: LoaderWaveFloat(size: size),
|
||||||
turns: Tween(begin: 0.0, end: 3.0).animate(_controller!),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 45,
|
|
||||||
child: SvgPicture.asset('assets/images/MyInfoMate_logo_only.svg')
|
|
||||||
)/*Icon(Icons.museum_outlined, color: kPrimaryColor, size: widget.iconSize == null ? size.height*0.1 : widget.iconSize!)*/,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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;
|
bool? isLoading = false;
|
||||||
UserRole? role;
|
UserRole? role;
|
||||||
|
|
||||||
|
bool get canEdit => role != null && role!.value <= 2;
|
||||||
|
|
||||||
ManagerAppContext({this.email, this.accessToken});
|
ManagerAppContext({this.email, this.accessToken});
|
||||||
|
|
||||||
// Implement toString to make it easier to see information about
|
// Implement toString to make it easier to see information about
|
||||||
|
|||||||
@ -19,8 +19,9 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
|||||||
List<Map<String, dynamic>> _keys = [];
|
List<Map<String, dynamic>> _keys = [];
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
|
|
||||||
static String _appTypeName(int? v) {
|
static String _appTypeName(dynamic v) {
|
||||||
switch (v) {
|
if (v is String) return v;
|
||||||
|
switch (v as int?) {
|
||||||
case 0: return 'VisitApp';
|
case 0: return 'VisitApp';
|
||||||
case 1: return 'TabletApp';
|
case 1: return 'TabletApp';
|
||||||
default: return 'Other';
|
default: return 'Other';
|
||||||
@ -42,9 +43,9 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _createKey(ManagerAppContext ctx, String name, ApiKeyAppType appType) async {
|
Future<String?> _createKey(ManagerAppContext ctx, String name, ApiKeyAppType appType, DateTime? dateExpiration) async {
|
||||||
final response = await ctx.clientAPI!.apiKeyApi!.apiKeyCreateApiKeyWithHttpInfo(
|
final response = await ctx.clientAPI!.apiKeyApi!.apiKeyCreateApiKeyWithHttpInfo(
|
||||||
CreateApiKeyRequest(name: name, appType: appType),
|
CreateApiKeyRequest(name: name, appType: appType, dateExpiration: dateExpiration),
|
||||||
);
|
);
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final Map<String, dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
|
final Map<String, dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
@ -62,6 +63,7 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
|||||||
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
|
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
|
||||||
final nameCtrl = TextEditingController();
|
final nameCtrl = TextEditingController();
|
||||||
ApiKeyAppType selectedType = ApiKeyAppType.number0;
|
ApiKeyAppType selectedType = ApiKeyAppType.number0;
|
||||||
|
DateTime? selectedExpiration;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -85,13 +87,44 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
selectedExpiration == null
|
||||||
|
? 'Pas d\'expiration'
|
||||||
|
: 'Expire le ${selectedExpiration!.day.toString().padLeft(2, '0')}/${selectedExpiration!.month.toString().padLeft(2, '0')}/${selectedExpiration!.year}',
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: ctx2,
|
||||||
|
initialDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
firstDate: DateTime.now().add(const Duration(days: 1)),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365 * 5)),
|
||||||
|
);
|
||||||
|
if (picked != null) setLocal(() => selectedExpiration = picked);
|
||||||
|
},
|
||||||
|
child: const Text('Choisir'),
|
||||||
|
),
|
||||||
|
if (selectedExpiration != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 16),
|
||||||
|
onPressed: () => setLocal(() => selectedExpiration = null),
|
||||||
|
tooltip: 'Supprimer l\'expiration',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx2), child: const Text('Annuler')),
|
TextButton(onPressed: () => Navigator.of(ctx2, rootNavigator: true).pop(), child: const Text('Annuler')),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(ctx2);
|
Navigator.of(ctx2, rootNavigator: true).pop();
|
||||||
final plainKey = await _createKey(ctx, nameCtrl.text, selectedType);
|
final plainKey = await _createKey(ctx, nameCtrl.text, selectedType, selectedExpiration);
|
||||||
if (plainKey != null && context.mounted) {
|
if (plainKey != null && context.mounted) {
|
||||||
_showPlainKeyDialog(context, plainKey);
|
_showPlainKeyDialog(context, plainKey);
|
||||||
}
|
}
|
||||||
@ -135,7 +168,7 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
|||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.of(context, rootNavigator: true).pop(),
|
||||||
child: const Text('J\'ai copié la clé'),
|
child: const Text('J\'ai copié la clé'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -146,15 +179,15 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
|||||||
void _confirmRevoke(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> key) {
|
void _confirmRevoke(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> key) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: const Text('Révoquer la clé API'),
|
title: const Text('Révoquer la clé API'),
|
||||||
content: Text('Révoquer « ${key['name']} » ? Les apps utilisant cette clé perdront l\'accès.'),
|
content: Text('Révoquer « ${key['name']} » ? Les apps utilisant cette clé perdront l\'accès.'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Annuler')),
|
TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Annuler')),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(dialogContext);
|
||||||
await _revokeKey(ctx, key['id'] as String);
|
await _revokeKey(ctx, key['id'] as String);
|
||||||
},
|
},
|
||||||
child: const Text('Révoquer', style: TextStyle(color: Colors.white)),
|
child: const Text('Révoquer', style: TextStyle(color: Colors.white)),
|
||||||
@ -199,47 +232,75 @@ class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
|||||||
const Center(child: Text('Aucune clé API'))
|
const Center(child: Text('Aucune clé API'))
|
||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: Card(
|
||||||
child: DataTable(
|
elevation: 0,
|
||||||
columns: const [
|
color: Colors.white,
|
||||||
DataColumn(label: Text('Nom')),
|
shape: RoundedRectangleBorder(
|
||||||
DataColumn(label: Text('Type')),
|
borderRadius: BorderRadius.circular(8),
|
||||||
DataColumn(label: Text('Créée le')),
|
side: BorderSide(color: Colors.grey.shade200),
|
||||||
DataColumn(label: Text('Statut')),
|
),
|
||||||
DataColumn(label: Text('Actions')),
|
clipBehavior: Clip.antiAlias,
|
||||||
],
|
child: SingleChildScrollView(
|
||||||
rows: _keys.map((key) {
|
child: SizedBox(
|
||||||
final isActive = key['isActive'] as bool? ?? false;
|
width: double.infinity,
|
||||||
final dateRaw = key['dateCreation'] as String?;
|
child: DataTable(
|
||||||
final date = dateRaw != null
|
horizontalMargin: 16,
|
||||||
? DateTime.tryParse(dateRaw)?.toLocal()
|
columnSpacing: 24,
|
||||||
: null;
|
headingRowColor: WidgetStateProperty.all(Colors.grey.shade50),
|
||||||
final dateStr = date != null
|
dividerThickness: 1,
|
||||||
? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'
|
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: [
|
return DataRow(cells: [
|
||||||
DataCell(Text(key['name'] as String? ?? '')),
|
DataCell(Text(key['name'] as String? ?? '')),
|
||||||
DataCell(Text(_appTypeName(key['appType'] as int?))),
|
DataCell(Text(_appTypeName(key['appType']))),
|
||||||
DataCell(Text(dateStr)),
|
DataCell(Text(dateStr, style: const TextStyle(color: Colors.grey))),
|
||||||
DataCell(Chip(
|
DataCell(Text(expStr, style: TextStyle(color: exp != null && exp.isBefore(DateTime.now()) ? Colors.red : Colors.grey))),
|
||||||
label: Text(isActive ? 'Active' : 'Révoquée',
|
DataCell(Chip(
|
||||||
style: TextStyle(color: isActive ? Colors.green.shade700 : Colors.red.shade700, fontSize: 12)),
|
label: Text(
|
||||||
backgroundColor: isActive ? Colors.green.shade50 : Colors.red.shade50,
|
isActive ? 'Active' : 'Révoquée',
|
||||||
side: BorderSide(color: isActive ? Colors.green.shade200 : Colors.red.shade200),
|
style: TextStyle(
|
||||||
)),
|
color: isActive ? Colors.green.shade700 : Colors.red.shade700,
|
||||||
DataCell(isActive
|
fontSize: 12,
|
||||||
? IconButton(
|
),
|
||||||
icon: const Icon(Icons.block, color: Colors.red),
|
),
|
||||||
tooltip: 'Révoquer',
|
backgroundColor: isActive ? Colors.green.shade50 : Colors.red.shade50,
|
||||||
onPressed: () => _confirmRevoke(context, managerCtx, key),
|
side: BorderSide(color: isActive ? Colors.green.shade200 : Colors.red.shade200),
|
||||||
)
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
: const SizedBox()),
|
visualDensity: VisualDensity.compact,
|
||||||
]);
|
)),
|
||||||
}).toList(),
|
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,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildAgendaHeader(size, mapProviderIn),
|
_buildAgendaHeader(size, mapProviderIn),
|
||||||
if (agendaDTO.isOnlineAgenda == false) ...[
|
...[
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding:
|
||||||
@ -94,7 +94,7 @@ class _AgendaConfigState extends State<AgendaConfig> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text("Évènements Manuels",
|
const Text("Évènements",
|
||||||
style:
|
style:
|
||||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
@ -146,7 +146,7 @@ class _AgendaConfigState extends State<AgendaConfig> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: events.isEmpty
|
child: events.isEmpty
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Text("Aucun évènement manuel",
|
child: Text("Aucun évènement",
|
||||||
style: TextStyle(fontStyle: FontStyle.italic)))
|
style: TextStyle(fontStyle: FontStyle.italic)))
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
itemCount: events.length,
|
itemCount: events.length,
|
||||||
@ -288,25 +288,40 @@ class _AgendaConfigState extends State<AgendaConfig> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SingleSelectContainer(
|
CheckInputContainer(
|
||||||
label: "Service carte :",
|
label: "Vue carte :",
|
||||||
color: Colors.black,
|
isChecked: agendaDTO.agendaMapProvider != null,
|
||||||
initialValue: mapProviderIn,
|
onChanged: (value) {
|
||||||
inputValues: const ["Google", "MapBox"],
|
|
||||||
onChanged: (String value) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
switch (value) {
|
if (value) {
|
||||||
case "Google":
|
agendaDTO.agendaMapProvider = MapProvider.Google;
|
||||||
agendaDTO.agendaMapProvider = MapProvider.Google;
|
} else {
|
||||||
break;
|
agendaDTO.agendaMapProvider = null;
|
||||||
case "MapBox":
|
|
||||||
agendaDTO.agendaMapProvider = MapProvider.MapBox;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
widget.onChanged(agendaDTO);
|
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)
|
if (agendaDTO.isOnlineAgenda == true)
|
||||||
MultiStringInputContainer(
|
MultiStringInputContainer(
|
||||||
label: "Fichiers json :",
|
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/multi_string_input_container.dart';
|
||||||
import 'package:manager_app/Components/string_input_container.dart';
|
import 'package:manager_app/Components/string_input_container.dart';
|
||||||
import 'package:manager_app/Components/geometry_input_container.dart';
|
import 'package:manager_app/Components/geometry_input_container.dart';
|
||||||
|
import 'package:manager_app/Components/resource_input_container.dart';
|
||||||
|
|
||||||
void showNewOrUpdateEventAgenda(
|
void showNewOrUpdateEventAgenda(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -95,6 +96,117 @@ void showNewOrUpdateEventAgenda(
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Divider(height: 24),
|
Divider(height: 24),
|
||||||
|
// Dates
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: halfWidth,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Date de début :"),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
icon: Icon(Icons.calendar_today, size: 16),
|
||||||
|
label: Text(workingEvent.dateFrom != null
|
||||||
|
? "${workingEvent.dateFrom!.day.toString().padLeft(2, '0')}/${workingEvent.dateFrom!.month.toString().padLeft(2, '0')}/${workingEvent.dateFrom!.year}"
|
||||||
|
: "Choisir une date"),
|
||||||
|
onPressed: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: workingEvent.dateFrom ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
if (picked != null) setState(() => workingEvent.dateFrom = picked);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: halfWidth,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Date de fin :"),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
icon: Icon(Icons.calendar_today, size: 16),
|
||||||
|
label: Text(workingEvent.dateTo != null
|
||||||
|
? "${workingEvent.dateTo!.day.toString().padLeft(2, '0')}/${workingEvent.dateTo!.month.toString().padLeft(2, '0')}/${workingEvent.dateTo!.year}"
|
||||||
|
: "Choisir une date"),
|
||||||
|
onPressed: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: workingEvent.dateTo ?? workingEvent.dateFrom ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
if (picked != null) setState(() => workingEvent.dateTo = picked);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(height: 24),
|
||||||
|
// Ressources
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: halfWidth,
|
||||||
|
child: ResourceInputContainer(
|
||||||
|
label: "Image :",
|
||||||
|
initialValue: workingEvent.resourceId,
|
||||||
|
inResourceTypes: const [ResourceType.Image, ResourceType.ImageUrl],
|
||||||
|
onChanged: (res) => setState(() {
|
||||||
|
workingEvent.resourceId = res.id;
|
||||||
|
workingEvent.resource = EventAgendaDTOResource.fromJson(res.toJson());
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: halfWidth,
|
||||||
|
child: ResourceInputContainer(
|
||||||
|
label: "Vidéo :",
|
||||||
|
initialValue: workingEvent.videoResourceId,
|
||||||
|
inResourceTypes: const [ResourceType.Video, ResourceType.VideoUrl],
|
||||||
|
onChanged: (res) => setState(() {
|
||||||
|
workingEvent.videoResourceId = res.id;
|
||||||
|
workingEvent.videoResource = EventAgendaDTOResource.fromJson(res.toJson());
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(height: 24),
|
||||||
|
// Vidéo externe
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: halfWidth,
|
||||||
|
child: StringInputContainer(
|
||||||
|
label: "ID YouTube :",
|
||||||
|
initialValue: workingEvent.idVideoYoutube ?? "",
|
||||||
|
onChanged: (val) => setState(() => workingEvent.idVideoYoutube = val.isEmpty ? null : val),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: halfWidth,
|
||||||
|
child: StringInputContainer(
|
||||||
|
label: "Lien vidéo direct :",
|
||||||
|
initialValue: workingEvent.videoLink ?? "",
|
||||||
|
onChanged: (val) => setState(() => workingEvent.videoLink = val.isEmpty ? null : val),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(height: 24),
|
||||||
// Site | Tel | Email
|
// Site | Tel | Email
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:manager_app/app_context.dart';
|
import 'package:manager_app/app_context.dart';
|
||||||
import 'package:manager_app/Models/managerContext.dart';
|
import 'package:manager_app/Models/managerContext.dart';
|
||||||
import 'package:manager_app/Components/message_notification.dart';
|
import 'package:manager_app/Components/message_notification.dart';
|
||||||
|
import 'package:manager_app/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart';
|
||||||
|
import 'package:manager_app/Components/dropDown_input_container.dart';
|
||||||
|
import 'showNewOrUpdateMapAnnotation.dart';
|
||||||
|
|
||||||
class EventConfig extends StatefulWidget {
|
class EventConfig extends StatefulWidget {
|
||||||
final SectionEventDTO initialValue;
|
final SectionEventDTO initialValue;
|
||||||
@ -25,12 +28,17 @@ class EventConfig extends StatefulWidget {
|
|||||||
|
|
||||||
class _EventConfigState extends State<EventConfig> {
|
class _EventConfigState extends State<EventConfig> {
|
||||||
late SectionEventDTO eventDTO;
|
late SectionEventDTO eventDTO;
|
||||||
|
List<SectionDTO> availableMaps = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
eventDTO = widget.initialValue;
|
eventDTO = widget.initialValue;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _loadProgrammeBlocks());
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_loadProgrammeBlocks();
|
||||||
|
_loadGlobalAnnotations();
|
||||||
|
_loadAvailableMaps();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadProgrammeBlocks() async {
|
Future<void> _loadProgrammeBlocks() async {
|
||||||
@ -48,6 +56,36 @@ class _EventConfigState extends State<EventConfig> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadGlobalAnnotations() async {
|
||||||
|
if (eventDTO.id == null || !mounted) return;
|
||||||
|
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||||
|
final api = (appContext.getContext() as ManagerAppContext).clientAPI!.sectionEventApi!;
|
||||||
|
try {
|
||||||
|
final annotations = await api.sectionEventGetGlobalMapAnnotations(eventDTO.id!);
|
||||||
|
if (annotations == null || !mounted) return;
|
||||||
|
setState(() {
|
||||||
|
eventDTO.globalMapAnnotations = annotations;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Silently keep initial value on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAvailableMaps() async {
|
||||||
|
if (eventDTO.configurationId == null || !mounted) return;
|
||||||
|
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||||
|
final api = (appContext.getContext() as ManagerAppContext).clientAPI!.sectionApi!;
|
||||||
|
try {
|
||||||
|
final sections = await api.sectionGetFromConfiguration(eventDTO.configurationId!);
|
||||||
|
if (sections == null || !mounted) return;
|
||||||
|
setState(() {
|
||||||
|
availableMaps = sections.where((s) => s.type == SectionType.Map).toList();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Silently keep empty on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
@ -182,6 +220,12 @@ class _EventConfigState extends State<EventConfig> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Divider(),
|
Divider(),
|
||||||
|
// --- Carte de base ---
|
||||||
|
_buildBaseSectionMapSection(),
|
||||||
|
Divider(),
|
||||||
|
// --- Annotations globales ---
|
||||||
|
_buildGlobalAnnotationsSection(),
|
||||||
|
Divider(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -383,7 +427,170 @@ class _EventConfigState extends State<EventConfig> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Divider(),
|
||||||
|
// --- Parcours ---
|
||||||
|
if (eventDTO.id != null)
|
||||||
|
ParcoursConfig(
|
||||||
|
initialValue: const [],
|
||||||
|
parentId: eventDTO.id!,
|
||||||
|
isEvent: true,
|
||||||
|
onChanged: (paths) {
|
||||||
|
// parcours are managed independently, no DTO update needed here
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBaseSectionMapSection() {
|
||||||
|
final mapItems = <String>['Aucune', ...availableMaps.map((m) => m.label ?? m.id ?? '')];
|
||||||
|
final currentValue = eventDTO.baseSectionMapId != null
|
||||||
|
? (availableMaps
|
||||||
|
.where((m) => m.id == eventDTO.baseSectionMapId)
|
||||||
|
.map((m) => m.label ?? m.id ?? '')
|
||||||
|
.firstOrNull ??
|
||||||
|
'Aucune')
|
||||||
|
: 'Aucune';
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("Carte de base",
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
DropDownInputContainer(
|
||||||
|
label: "Carte :",
|
||||||
|
values: mapItems,
|
||||||
|
initialValue: currentValue,
|
||||||
|
onChange: (val) {
|
||||||
|
setState(() {
|
||||||
|
if (val == 'Aucune') {
|
||||||
|
eventDTO.baseSectionMapId = null;
|
||||||
|
} else {
|
||||||
|
final match = availableMaps.firstWhere(
|
||||||
|
(m) => (m.label ?? m.id ?? '') == val,
|
||||||
|
orElse: () => availableMaps.first);
|
||||||
|
eventDTO.baseSectionMapId = match.id;
|
||||||
|
}
|
||||||
|
widget.onChanged(eventDTO);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGlobalAnnotationsSection() {
|
||||||
|
final annotations = eventDTO.globalMapAnnotations ?? [];
|
||||||
|
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text("Annotations globales",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text("Ajouter une annotation"),
|
||||||
|
onPressed: eventDTO.id == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
showNewOrUpdateMapAnnotation(context, null,
|
||||||
|
(newAnnotation) async {
|
||||||
|
try {
|
||||||
|
final created = await (appContext.getContext()
|
||||||
|
as ManagerAppContext)
|
||||||
|
.clientAPI!
|
||||||
|
.sectionEventApi!
|
||||||
|
.sectionEventCreateGlobalMapAnnotation(
|
||||||
|
eventDTO.id!, newAnnotation);
|
||||||
|
if (created != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
eventDTO.globalMapAnnotations = [
|
||||||
|
...annotations,
|
||||||
|
created
|
||||||
|
];
|
||||||
|
});
|
||||||
|
showNotification(kSuccess, kWhite,
|
||||||
|
'Annotation créée avec succès', context, null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showNotification(kError, kWhite,
|
||||||
|
'Erreur lors de la création de l\'annotation',
|
||||||
|
context, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: kSuccess,
|
||||||
|
foregroundColor: kWhite,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (annotations.isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
|
child: Text("Aucune annotation globale",
|
||||||
|
style: TextStyle(fontStyle: FontStyle.italic)),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
...annotations.asMap().entries.map((entry) {
|
||||||
|
final idx = entry.key;
|
||||||
|
final ann = entry.value;
|
||||||
|
final labelText = ann.label != null && ann.label!.isNotEmpty
|
||||||
|
? (ann.label!.firstWhere((t) => t.language == 'FR',
|
||||||
|
orElse: () => ann.label![0])
|
||||||
|
.value ?? 'Annotation ${idx + 1}')
|
||||||
|
: 'Annotation ${idx + 1}';
|
||||||
|
return Card(
|
||||||
|
margin:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.place, color: kPrimaryColor),
|
||||||
|
title: Text(labelText),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.delete, color: kError),
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
if (ann.id != null) {
|
||||||
|
await (appContext.getContext() as ManagerAppContext)
|
||||||
|
.clientAPI!
|
||||||
|
.sectionEventApi!
|
||||||
|
.sectionEventDeleteMapAnnotation(ann.id!);
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
final updated = List<MapAnnotationDTO>.from(
|
||||||
|
eventDTO.globalMapAnnotations ?? []);
|
||||||
|
updated.removeAt(idx);
|
||||||
|
eventDTO.globalMapAnnotations = updated;
|
||||||
|
});
|
||||||
|
showNotification(kSuccess, kWhite,
|
||||||
|
'Annotation supprimée', context, null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showNotification(kError, kWhite,
|
||||||
|
'Erreur lors de la suppression', context, null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:flutter/material.dart';
|
||||||
import 'package:manager_api_new/api.dart';
|
import 'package:manager_api_new/api.dart';
|
||||||
import 'package:manager_app/constants.dart';
|
import 'package:manager_app/constants.dart';
|
||||||
|
import 'package:manager_app/Components/confirmation_dialog.dart';
|
||||||
import 'package:manager_app/Components/rounded_button.dart';
|
import 'package:manager_app/Components/rounded_button.dart';
|
||||||
import 'package:manager_app/Components/multi_string_input_container.dart';
|
import 'package:manager_app/Components/multi_string_input_container.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'showNewOrUpdateMapAnnotation.dart';
|
||||||
|
|
||||||
void showNewOrUpdateProgrammeBlock(
|
void showNewOrUpdateProgrammeBlock(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -17,14 +19,21 @@ void showNewOrUpdateProgrammeBlock(
|
|||||||
description: List<TranslationDTO>.from(block.description ?? []),
|
description: List<TranslationDTO>.from(block.description ?? []),
|
||||||
startTime: block.startTime,
|
startTime: block.startTime,
|
||||||
endTime: block.endTime,
|
endTime: block.endTime,
|
||||||
|
mapAnnotations: List<MapAnnotation>.from(block.mapAnnotations ?? []),
|
||||||
)
|
)
|
||||||
: ProgrammeBlock(
|
: ProgrammeBlock(
|
||||||
title: [],
|
title: [],
|
||||||
description: [],
|
description: [],
|
||||||
startTime: DateTime.now(),
|
startTime: DateTime.now(),
|
||||||
endTime: DateTime.now().add(Duration(hours: 1)),
|
endTime: DateTime.now().add(Duration(hours: 1)),
|
||||||
|
mapAnnotations: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
MapAnnotationDTO _toDTO(MapAnnotation a) =>
|
||||||
|
MapAnnotationDTO.fromJson(a.toJson())!;
|
||||||
|
MapAnnotation _fromDTO(MapAnnotationDTO a) =>
|
||||||
|
MapAnnotation.fromJson(a.toJson())!;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
@ -198,6 +207,119 @@ void showNewOrUpdateProgrammeBlock(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Divider(height: 24),
|
||||||
|
// Annotations
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("Annotations",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15)),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.add_circle_outline,
|
||||||
|
color: kSuccess),
|
||||||
|
onPressed: () {
|
||||||
|
showNewOrUpdateMapAnnotation(
|
||||||
|
context,
|
||||||
|
null,
|
||||||
|
(newAnnotation) {
|
||||||
|
setState(() {
|
||||||
|
workingBlock.mapAnnotations = [
|
||||||
|
...(workingBlock.mapAnnotations ?? []),
|
||||||
|
_fromDTO(newAnnotation),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (workingBlock.mapAnnotations == null ||
|
||||||
|
workingBlock.mapAnnotations!.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
"Aucune annotation configurée.",
|
||||||
|
style: TextStyle(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount:
|
||||||
|
workingBlock.mapAnnotations!.length,
|
||||||
|
itemBuilder: (context, aIndex) {
|
||||||
|
final annotation =
|
||||||
|
workingBlock.mapAnnotations![aIndex];
|
||||||
|
final label = annotation.label
|
||||||
|
?.firstWhere(
|
||||||
|
(t) => t.language == 'FR',
|
||||||
|
orElse: () =>
|
||||||
|
annotation.label!.first,
|
||||||
|
)
|
||||||
|
.value ??
|
||||||
|
"Annotation $aIndex";
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(label),
|
||||||
|
subtitle: Text(
|
||||||
|
annotation.geometryType?.value == 1
|
||||||
|
? 'Polyline'
|
||||||
|
: annotation.geometryType
|
||||||
|
?.value ==
|
||||||
|
2
|
||||||
|
? 'Circle'
|
||||||
|
: annotation.geometryType
|
||||||
|
?.value ==
|
||||||
|
3
|
||||||
|
? 'Polygon'
|
||||||
|
: 'Point'),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.edit,
|
||||||
|
size: 18,
|
||||||
|
color: kPrimaryColor),
|
||||||
|
onPressed: () {
|
||||||
|
showNewOrUpdateMapAnnotation(
|
||||||
|
context,
|
||||||
|
_toDTO(annotation),
|
||||||
|
(updated) {
|
||||||
|
setState(() {
|
||||||
|
workingBlock.mapAnnotations![
|
||||||
|
aIndex] =
|
||||||
|
_fromDTO(updated);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.delete,
|
||||||
|
size: 18, color: kError),
|
||||||
|
onPressed: () {
|
||||||
|
showConfirmationDialog(
|
||||||
|
"Supprimer cette annotation ?",
|
||||||
|
() {},
|
||||||
|
() => setState(() => workingBlock
|
||||||
|
.mapAnnotations!
|
||||||
|
.removeAt(aIndex)),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import 'package:manager_app/Components/fetch_section_icon.dart';
|
|||||||
import 'package:manager_app/Components/resource_input_container.dart';
|
import 'package:manager_app/Components/resource_input_container.dart';
|
||||||
import 'package:manager_app/Components/multi_select_container.dart';
|
import 'package:manager_app/Components/multi_select_container.dart';
|
||||||
import 'package:manager_app/Components/single_select_container.dart';
|
import 'package:manager_app/Components/single_select_container.dart';
|
||||||
|
import 'package:manager_app/Components/check_input_container.dart';
|
||||||
import 'package:manager_app/Components/slider_input_container.dart';
|
import 'package:manager_app/Components/slider_input_container.dart';
|
||||||
import 'package:manager_app/Components/string_input_container.dart';
|
import 'package:manager_app/Components/string_input_container.dart';
|
||||||
import 'package:manager_app/Screens/Configurations/Section/SubSection/Map/showNewOrUpdateGeoPoint.dart';
|
import 'package:manager_app/Screens/Configurations/Section/SubSection/Map/showNewOrUpdateGeoPoint.dart';
|
||||||
@ -662,6 +663,16 @@ class _MapConfigState extends State<MapConfig> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
|
CheckInputContainer(
|
||||||
|
label: "Vue liste :",
|
||||||
|
isChecked: mapDTO.isListViewEnabled ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
mapDTO.isListViewEnabled = value;
|
||||||
|
widget.onChanged(mapDTO);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
if (mapDTO.mapProvider == MapProvider.Google)
|
if (mapDTO.mapProvider == MapProvider.Google)
|
||||||
DropDownInputContainer(
|
DropDownInputContainer(
|
||||||
label: "Type :",
|
label: "Type :",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'package:manager_app/Components/confirmation_dialog.dart';
|
|||||||
import 'package:manager_app/Components/geometry_input_container.dart';
|
import 'package:manager_app/Components/geometry_input_container.dart';
|
||||||
import 'package:manager_app/Components/multi_string_input_container.dart';
|
import 'package:manager_app/Components/multi_string_input_container.dart';
|
||||||
import 'package:manager_app/Components/rounded_button.dart';
|
import 'package:manager_app/Components/rounded_button.dart';
|
||||||
|
import 'package:manager_app/Components/string_input_container.dart';
|
||||||
import 'package:manager_app/constants.dart';
|
import 'package:manager_app/constants.dart';
|
||||||
import 'showNewOrUpdateQuizQuestion.dart';
|
import 'showNewOrUpdateQuizQuestion.dart';
|
||||||
|
|
||||||
@ -108,6 +109,78 @@ void showNewOrUpdateGuidedStep(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Divider(height: 24),
|
||||||
|
// Comportement de l'étape
|
||||||
|
Text("Comportement", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SwitchListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text("Cachée initialement"),
|
||||||
|
value: workingStep.isHiddenInitially ?? false,
|
||||||
|
onChanged: (val) => setState(() => workingStep.isHiddenInitially = val),
|
||||||
|
activeThumbColor: kPrimaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SwitchListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text("Verrouillée"),
|
||||||
|
value: workingStep.isStepLocked ?? false,
|
||||||
|
onChanged: (val) => setState(() => workingStep.isStepLocked = val),
|
||||||
|
activeThumbColor: kPrimaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SwitchListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text("Timer"),
|
||||||
|
value: workingStep.isStepTimer ?? false,
|
||||||
|
onChanged: (val) => setState(() => workingStep.isStepTimer = val),
|
||||||
|
activeThumbColor: kPrimaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (workingStep.isStepTimer == true) ...[
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: halfWidth,
|
||||||
|
child: StringInputContainer(
|
||||||
|
label: "Durée (secondes) :",
|
||||||
|
initialValue: workingStep.timerSeconds?.toString() ?? "",
|
||||||
|
onChanged: (val) => setState(() =>
|
||||||
|
workingStep.timerSeconds = int.tryParse(val)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: halfWidth,
|
||||||
|
child: MultiStringInputContainer(
|
||||||
|
label: "Message d'expiration :",
|
||||||
|
modalLabel: "Message d'expiration du timer",
|
||||||
|
initialValue: workingStep.timerExpiredMessage ?? [],
|
||||||
|
onGetResult: (val) => setState(() =>
|
||||||
|
workingStep.timerExpiredMessage = val.isEmpty ? null : val),
|
||||||
|
maxLines: 2,
|
||||||
|
isTitle: false,
|
||||||
|
isHTML: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SizedBox(height: 8),
|
||||||
|
StringInputContainer(
|
||||||
|
label: "GeoPoint de déclenchement (ID) :",
|
||||||
|
initialValue: workingStep.triggerGeoPointId?.toString() ?? "",
|
||||||
|
onChanged: (val) => setState(() =>
|
||||||
|
workingStep.triggerGeoPointId = int.tryParse(val)),
|
||||||
|
),
|
||||||
// Questions — uniquement en mode Escape Game
|
// Questions — uniquement en mode Escape Game
|
||||||
if (isEscapeMode) ...[
|
if (isEscapeMode) ...[
|
||||||
Divider(height: 24),
|
Divider(height: 24),
|
||||||
|
|||||||
@ -391,6 +391,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getButtons(SectionDTO sectionDTO, AppContext appContext) {
|
getButtons(SectionDTO sectionDTO, AppContext appContext) {
|
||||||
|
final canEdit = (appContext.getContext() as ManagerAppContext).canEdit;
|
||||||
return Align(
|
return Align(
|
||||||
alignment: AlignmentDirectional.bottomCenter,
|
alignment: AlignmentDirectional.bottomCenter,
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -409,7 +410,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
if (canEdit) Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: RoundedButton(
|
child: RoundedButton(
|
||||||
text: "Supprimer",
|
text: "Supprimer",
|
||||||
@ -422,7 +423,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
if (canEdit) Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: RoundedButton(
|
child: RoundedButton(
|
||||||
text: "Sauvegarder",
|
text: "Sauvegarder",
|
||||||
|
|||||||
@ -346,6 +346,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getButtons(ConfigurationDTO configurationDTO, AppContext appContext) {
|
getButtons(ConfigurationDTO configurationDTO, AppContext appContext) {
|
||||||
|
final canEdit = (appContext.getContext() as ManagerAppContext).canEdit;
|
||||||
return Align(
|
return Align(
|
||||||
alignment: AlignmentDirectional.bottomCenter,
|
alignment: AlignmentDirectional.bottomCenter,
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -364,7 +365,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
if (canEdit) Padding(
|
||||||
padding: const EdgeInsets.all(5.0),
|
padding: const EdgeInsets.all(5.0),
|
||||||
child: RoundedButton(
|
child: RoundedButton(
|
||||||
text: "Supprimer",
|
text: "Supprimer",
|
||||||
@ -377,7 +378,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
if (canEdit) Padding(
|
||||||
padding: const EdgeInsets.all(5.0),
|
padding: const EdgeInsets.all(5.0),
|
||||||
child: RoundedButton(
|
child: RoundedButton(
|
||||||
text: "Sauvegarder",
|
text: "Sauvegarder",
|
||||||
|
|||||||
@ -43,7 +43,7 @@ class _ConfigurationsScreenState extends State<ConfigurationsScreen> {
|
|||||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
var tempOutput = new List<ConfigurationDTO>.from(snapshot.data);
|
var tempOutput = new List<ConfigurationDTO>.from(snapshot.data);
|
||||||
tempOutput.add(ConfigurationDTO(id: null));
|
if (managerAppContext.canEdit) tempOutput.add(ConfigurationDTO(id: null));
|
||||||
return bodyGrid(tempOutput, size, appContext, context);
|
return bodyGrid(tempOutput, size, appContext, context);
|
||||||
} else if (snapshot.connectionState == ConnectionState.none) {
|
} else if (snapshot.connectionState == ConnectionState.none) {
|
||||||
return Text("No data");
|
return Text("No data");
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import 'package:manager_app/Screens/Kiosk_devices/kiosk_screen.dart';
|
|||||||
import 'package:manager_app/Screens/Resources/resources_screen.dart';
|
import 'package:manager_app/Screens/Resources/resources_screen.dart';
|
||||||
import 'package:manager_app/Screens/Statistics/statistics_screen.dart';
|
import 'package:manager_app/Screens/Statistics/statistics_screen.dart';
|
||||||
import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart';
|
import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart';
|
||||||
|
import 'package:manager_app/Screens/Notifications/notifications_screen.dart';
|
||||||
import 'package:manager_app/Screens/Users/users_screen.dart';
|
import 'package:manager_app/Screens/Users/users_screen.dart';
|
||||||
import 'package:manager_app/app_context.dart';
|
import 'package:manager_app/app_context.dart';
|
||||||
import 'package:manager_app/constants.dart';
|
import 'package:manager_app/constants.dart';
|
||||||
@ -40,6 +41,11 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
|
|
||||||
final ValueNotifier<int?> currentPosition = ValueNotifier<int?>(null);
|
final ValueNotifier<int?> currentPosition = ValueNotifier<int?>(null);
|
||||||
|
|
||||||
|
// Keys for nudge animation — one per section type
|
||||||
|
final _nudgeKeys = <String, GlobalKey<_NudgeIconState>>{};
|
||||||
|
GlobalKey<_NudgeIconState> _nudgeKey(String type) =>
|
||||||
|
_nudgeKeys.putIfAbsent(type, () => GlobalKey<_NudgeIconState>());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -115,7 +121,7 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
onTap: isCurrent
|
onTap: isCurrent
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
Navigator.pop(context);
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
final newInstance = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id);
|
final newInstance = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id);
|
||||||
if (newInstance != null && context.mounted) {
|
if (newInstance != null && context.mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -135,12 +141,25 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Fermer')),
|
TextButton(onPressed: () => Navigator.of(context, rootNavigator: true).pop(), child: const Text('Fermer')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static IconData _sectionIcon(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'devices': return Icons.apps;
|
||||||
|
case 'configurations': return Icons.settings_outlined;
|
||||||
|
case 'resources': return Icons.folder_open_outlined;
|
||||||
|
case 'statistics': return Icons.bar_chart;
|
||||||
|
case 'notifications': return Icons.notifications_none;
|
||||||
|
case 'users': return Icons.people_outline;
|
||||||
|
case 'apikeys': return Icons.vpn_key_outlined;
|
||||||
|
default: return Icons.circle_outlined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildMenu(BuildContext context, AppContext appContext, ManagerAppContext managerAppContext, bool isDrawer) {
|
Widget buildMenu(BuildContext context, AppContext appContext, ManagerAppContext managerAppContext, bool isDrawer) {
|
||||||
return Container(
|
return Container(
|
||||||
width: isDrawer ? null : 250, // fixed width on sidebar, null on drawer for full width
|
width: isDrawer ? null : 250, // fixed width on sidebar, null on drawer for full width
|
||||||
@ -190,6 +209,7 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
|
|
||||||
if (section.subMenu.isEmpty) {
|
if (section.subMenu.isEmpty) {
|
||||||
return Container(
|
return Container(
|
||||||
|
key: ValueKey(section.type),
|
||||||
decoration: currentPath!.contains(section.type)
|
decoration: currentPath!.contains(section.type)
|
||||||
? BoxDecoration(
|
? BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
@ -201,21 +221,38 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
leading: _NudgeIcon(
|
||||||
|
key: _nudgeKey(section.type),
|
||||||
|
icon: _sectionIcon(section.type),
|
||||||
|
isActive: currentPath.contains(section.type),
|
||||||
|
color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
title: Text(section.name, style: TextStyle(color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: currentPath.contains(section.type) ? FontWeight.w500 : FontWeight.w100)),
|
title: Text(section.name, style: TextStyle(color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: currentPath.contains(section.type) ? FontWeight.w500 : FontWeight.w100)),
|
||||||
selected: currentPosition == section.menuId,
|
selected: currentPosition == section.menuId,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
//currentPosition.value = section.menuId;
|
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
||||||
|
_nudgeKey(section.type).currentState?.nudge());
|
||||||
context.go('/main/${section.type}');
|
context.go('/main/${section.type}');
|
||||||
if (isDrawer) Navigator.of(context).pop(); // Close drawer on mobile
|
if (isDrawer) Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
final isAppActive = currentPath!.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr");
|
||||||
return Container(
|
return Container(
|
||||||
|
key: ValueKey(section.type),
|
||||||
child: ExpansionTile(
|
child: ExpansionTile(
|
||||||
iconColor: currentPath!.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? kPrimaryColor : kBodyTextColor,
|
leading: _NudgeIcon(
|
||||||
collapsedIconColor: currentPath.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? kPrimaryColor : kBodyTextColor,
|
key: _nudgeKey(section.type),
|
||||||
title: Text(section.name, style: TextStyle(color: currentPath.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: currentPath.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr") ? FontWeight.w500 : FontWeight.w100)),
|
icon: _sectionIcon(section.type),
|
||||||
|
isActive: isAppActive,
|
||||||
|
color: isAppActive ? kPrimaryColor : kBodyTextColor,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
iconColor: isAppActive ? kPrimaryColor : kBodyTextColor,
|
||||||
|
collapsedIconColor: isAppActive ? kPrimaryColor : kBodyTextColor,
|
||||||
|
title: Text(section.name, style: TextStyle(color: isAppActive ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: isAppActive ? FontWeight.w500 : FontWeight.w100)),
|
||||||
children: section.subMenu.map((subSection) {
|
children: section.subMenu.map((subSection) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: currentPath.contains(subSection.type)
|
decoration: currentPath.contains(subSection.type)
|
||||||
@ -246,11 +283,11 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
),
|
),
|
||||||
selected: currentPosition.value == subSection.menuId,
|
selected: currentPosition.value == subSection.menuId,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (currentPath != null && currentPath.contains(subSection.type)) {
|
||||||
|
// already on this page
|
||||||
if(currentPath != null && currentPath.contains(subSection.type)) {
|
|
||||||
// DO NOTHING, we are already display the correct interface
|
|
||||||
} else {
|
} else {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
||||||
|
_nudgeKey(section.type).currentState?.nudge());
|
||||||
context.go('/main/${subSection.type}');
|
context.go('/main/${subSection.type}');
|
||||||
}
|
}
|
||||||
if (isDrawer) Navigator.of(context).pop();
|
if (isDrawer) Navigator.of(context).pop();
|
||||||
@ -316,6 +353,12 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
// Synchronise les items de menu sensibles au rôle à chaque rebuild
|
// Synchronise les items de menu sensibles au rôle à chaque rebuild
|
||||||
final role = managerAppContext.role;
|
final role = managerAppContext.role;
|
||||||
final hasAdminItems = menu.sections!.any((s) => s.menuId == 8);
|
final hasAdminItems = menu.sections!.any((s) => s.menuId == 8);
|
||||||
|
final hasNotifItem = menu.sections!.any((s) => s.menuId == 10);
|
||||||
|
if (role != null && role.value <= 1 && managerAppContext.instanceDTO?.isPushNotification == true && !hasNotifItem) {
|
||||||
|
menu.sections!.add(MenuSection(name: "Notifications", type: "notifications", menuId: 10, subMenu: []));
|
||||||
|
} else if ((role == null || role.value > 1 || managerAppContext.instanceDTO?.isPushNotification != true) && hasNotifItem) {
|
||||||
|
menu.sections!.removeWhere((s) => s.menuId == 10);
|
||||||
|
}
|
||||||
if (role != null && role.value <= 1 && !hasAdminItems) {
|
if (role != null && role.value <= 1 && !hasAdminItems) {
|
||||||
menu.sections!.add(MenuSection(name: "Utilisateurs", type: "users", menuId: 8, subMenu: []));
|
menu.sections!.add(MenuSection(name: "Utilisateurs", type: "users", menuId: 8, subMenu: []));
|
||||||
menu.sections!.add(MenuSection(name: "Clés API", type: "apikeys", menuId: 9, subMenu: []));
|
menu.sections!.add(MenuSection(name: "Clés API", type: "apikeys", menuId: 9, subMenu: []));
|
||||||
@ -403,6 +446,9 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
case "apikeys":
|
case "apikeys":
|
||||||
currentPosition = 9;
|
currentPosition = 9;
|
||||||
break;
|
break;
|
||||||
|
case "notifications":
|
||||||
|
currentPosition = 10;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,7 +519,59 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
child: ApiKeysScreen()
|
child: ApiKeysScreen()
|
||||||
);
|
);
|
||||||
|
case 'notifications':
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: NotificationsScreen()
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return Text('Hellow default');
|
return Text('Hellow default');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _NudgeIcon extends StatefulWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final bool isActive;
|
||||||
|
final Color color;
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
const _NudgeIcon({Key? key, required this.icon, required this.isActive, required this.color, required this.size}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_NudgeIcon> createState() => _NudgeIconState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NudgeIconState extends State<_NudgeIcon> with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _ctrl;
|
||||||
|
late final Animation<double> _dx;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
|
||||||
|
_dx = TweenSequence<double>([
|
||||||
|
TweenSequenceItem(tween: Tween(begin: 0.0, end: 5.0), weight: 35),
|
||||||
|
TweenSequenceItem(tween: Tween(begin: 5.0, end: 0.0), weight: 65),
|
||||||
|
]).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
|
||||||
|
}
|
||||||
|
|
||||||
|
void nudge() => _ctrl.forward(from: 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _dx,
|
||||||
|
builder: (_, child) => Transform.translate(
|
||||||
|
offset: Offset(_dx.value, 0),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: Icon(widget.icon, color: widget.color, size: widget.size),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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/fetch_resource_icon.dart';
|
||||||
import 'package:manager_app/Components/multi_select_container.dart';
|
import 'package:manager_app/Components/multi_select_container.dart';
|
||||||
import 'package:manager_app/Components/string_input_container.dart';
|
import 'package:manager_app/Components/string_input_container.dart';
|
||||||
|
import 'package:manager_app/Models/managerContext.dart';
|
||||||
import 'package:manager_app/app_context.dart';
|
import 'package:manager_app/app_context.dart';
|
||||||
import 'package:manager_app/constants.dart';
|
import 'package:manager_app/constants.dart';
|
||||||
import 'package:manager_api_new/api.dart';
|
import 'package:manager_api_new/api.dart';
|
||||||
@ -89,7 +90,7 @@ class _ResourceBodyGridState extends State<ResourceBodyGrid> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.isAddButton)
|
if (widget.isAddButton && (appContext.getContext() as ManagerAppContext).canEdit)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onSelect(ResourceDTO(id: widget.isSelectModal ? "-1" : null));
|
widget.onSelect(ResourceDTO(id: widget.isSelectModal ? "-1" : null));
|
||||||
|
|||||||
@ -431,18 +431,22 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTablesRow(StatsSummaryDTO stats) {
|
Widget _buildTablesRow(StatsSummaryDTO stats) {
|
||||||
return Row(
|
final tables = <Widget>[
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
if (stats.topPois.isNotEmpty) Expanded(child: _buildPoiTable(stats)),
|
||||||
children: [
|
if (stats.topAgendaEvents.isNotEmpty) Expanded(child: _buildAgendaTable(stats)),
|
||||||
if (stats.topPois.isNotEmpty) Expanded(child: _buildPoiTable(stats)),
|
if (stats.quizStats.isNotEmpty) Expanded(child: _buildQuizTable(stats)),
|
||||||
if (stats.topPois.isNotEmpty && stats.topAgendaEvents.isNotEmpty) const SizedBox(width: 12),
|
if (stats.gameStats.isNotEmpty) Expanded(child: _buildGameTable(stats)),
|
||||||
if (stats.topAgendaEvents.isNotEmpty) Expanded(child: _buildAgendaTable(stats)),
|
if (stats.topArticles.isNotEmpty) Expanded(child: _buildArticleTable(stats)),
|
||||||
if ((stats.topPois.isNotEmpty || stats.topAgendaEvents.isNotEmpty) && stats.quizStats.isNotEmpty) const SizedBox(width: 12),
|
if (stats.topMenuItems.isNotEmpty) Expanded(child: _buildMenuTable(stats)),
|
||||||
if (stats.quizStats.isNotEmpty) Expanded(child: _buildQuizTable(stats)),
|
if (stats.qrScans.totalScans > 0) Expanded(child: _buildQrCard(stats)),
|
||||||
if ((stats.topPois.isNotEmpty || stats.topAgendaEvents.isNotEmpty || stats.quizStats.isNotEmpty) && stats.gameStats.isNotEmpty) const SizedBox(width: 12),
|
];
|
||||||
if (stats.gameStats.isNotEmpty) Expanded(child: _buildGameTable(stats)),
|
if (tables.isEmpty) return const SizedBox();
|
||||||
],
|
final spaced = <Widget>[];
|
||||||
);
|
for (var i = 0; i < tables.length; i++) {
|
||||||
|
spaced.add(tables[i]);
|
||||||
|
if (i < tables.length - 1) spaced.add(const SizedBox(width: 12));
|
||||||
|
}
|
||||||
|
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: spaced);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPoiTable(StatsSummaryDTO stats) {
|
Widget _buildPoiTable(StatsSummaryDTO stats) {
|
||||||
@ -475,6 +479,53 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
|
|||||||
]).toList());
|
]).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildArticleTable(StatsSummaryDTO stats) {
|
||||||
|
return _tableCard('Articles lus', ['Section', 'Lectures'], stats.topArticles.map((a) => [
|
||||||
|
a.sectionId ?? '—',
|
||||||
|
'${a.reads}',
|
||||||
|
]).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMenuTable(StatsSummaryDTO stats) {
|
||||||
|
return _tableCard('Menu', ['Item', 'Taps'], stats.topMenuItems.map((m) => [
|
||||||
|
m.menuItemTitle ?? m.targetSectionId ?? '—',
|
||||||
|
'${m.taps}',
|
||||||
|
]).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQrCard(StatsSummaryDTO stats) {
|
||||||
|
final qr = stats.qrScans;
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: kWhite,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('QR Scans', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: kPrimaryColor)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_qrRow(Icons.qr_code_scanner, 'Total', '${qr.totalScans}', kPrimaryColor),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_qrRow(Icons.check_circle_outline, 'Valides', '${qr.validScans}', Colors.green.shade600),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_qrRow(Icons.cancel_outlined, 'Invalides', '${qr.invalidScans}', Colors.red.shade400),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _qrRow(IconData icon, String label, String value, Color color) {
|
||||||
|
return Row(children: [
|
||||||
|
Icon(icon, size: 16, color: color),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(child: Text(label, style: TextStyle(fontSize: 13, color: kBodyTextColor))),
|
||||||
|
Text(value, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: color)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _tableCard(String title, List<String> headers, List<List<String>> rows) {
|
Widget _tableCard(String title, List<String> headers, List<List<String>> rows) {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|||||||
@ -17,14 +17,23 @@ class _UsersScreenState extends State<UsersScreen> {
|
|||||||
List<Map<String, dynamic>> _users = [];
|
List<Map<String, dynamic>> _users = [];
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
|
|
||||||
static String _roleName(int? v) {
|
static const _roleNames = ['SuperAdmin', 'InstanceAdmin', 'ContentEditor', 'Viewer'];
|
||||||
switch (v) {
|
|
||||||
case 0: return 'SuperAdmin';
|
static String _roleName(dynamic v) {
|
||||||
case 1: return 'InstanceAdmin';
|
if (v is String) return v;
|
||||||
case 2: return 'ContentEditor';
|
final i = v as int?;
|
||||||
case 3: return 'Viewer';
|
if (i != null && i >= 0 && i < _roleNames.length) return _roleNames[i];
|
||||||
default: return '—';
|
return '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertit une valeur de rôle (String ou int) en int pour les comparaisons
|
||||||
|
static int _roleToInt(dynamic v) {
|
||||||
|
if (v is int) return v;
|
||||||
|
if (v is String) {
|
||||||
|
final i = _roleNames.indexOf(v);
|
||||||
|
return i >= 0 ? i : 3;
|
||||||
}
|
}
|
||||||
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Roles the caller is allowed to assign (can't assign higher than own role)
|
// Roles the caller is allowed to assign (can't assign higher than own role)
|
||||||
@ -79,7 +88,7 @@ class _UsersScreenState extends State<UsersScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
|
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
|
||||||
final callerRole = ctx.role?.value ?? 2;
|
final callerRole = _roleToInt(ctx.role?.value);
|
||||||
final emailCtrl = TextEditingController();
|
final emailCtrl = TextEditingController();
|
||||||
final firstCtrl = TextEditingController();
|
final firstCtrl = TextEditingController();
|
||||||
final lastCtrl = TextEditingController();
|
final lastCtrl = TextEditingController();
|
||||||
@ -131,10 +140,10 @@ class _UsersScreenState extends State<UsersScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showEditDialog(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> user) {
|
void _showEditDialog(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> user) {
|
||||||
final callerRole = ctx.role?.value ?? 2;
|
final callerRole = _roleToInt(ctx.role?.value);
|
||||||
final firstCtrl = TextEditingController(text: user['firstName'] as String? ?? '');
|
final firstCtrl = TextEditingController(text: user['firstName'] as String? ?? '');
|
||||||
final lastCtrl = TextEditingController(text: user['lastName'] as String? ?? '');
|
final lastCtrl = TextEditingController(text: user['lastName'] as String? ?? '');
|
||||||
int selectedRole = (user['role'] as int?) ?? callerRole;
|
int selectedRole = _roleToInt(user['role']);
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -208,6 +217,15 @@ class _UsersScreenState extends State<UsersScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Color _roleColor(dynamic v) {
|
||||||
|
switch (_roleToInt(v)) {
|
||||||
|
case 0: return Colors.purple;
|
||||||
|
case 1: return Colors.blue;
|
||||||
|
case 2: return Colors.teal;
|
||||||
|
default: return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appContext = Provider.of<AppContext>(context);
|
final appContext = Provider.of<AppContext>(context);
|
||||||
@ -234,36 +252,64 @@ class _UsersScreenState extends State<UsersScreen> {
|
|||||||
const Center(child: Text('Aucun utilisateur'))
|
const Center(child: Text('Aucun utilisateur'))
|
||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: Card(
|
||||||
child: DataTable(
|
elevation: 0,
|
||||||
columns: const [
|
color: Colors.white,
|
||||||
DataColumn(label: Text('Email')),
|
shape: RoundedRectangleBorder(
|
||||||
DataColumn(label: Text('Prénom')),
|
borderRadius: BorderRadius.circular(8),
|
||||||
DataColumn(label: Text('Nom')),
|
side: BorderSide(color: Colors.grey.shade200),
|
||||||
DataColumn(label: Text('Rôle')),
|
),
|
||||||
DataColumn(label: Text('Actions')),
|
clipBehavior: Clip.antiAlias,
|
||||||
],
|
child: SingleChildScrollView(
|
||||||
rows: _users.map((user) {
|
child: SizedBox(
|
||||||
return DataRow(cells: [
|
width: double.infinity,
|
||||||
DataCell(Text(user['email'] as String? ?? '')),
|
child: DataTable(
|
||||||
DataCell(Text(user['firstName'] as String? ?? '')),
|
horizontalMargin: 16,
|
||||||
DataCell(Text(user['lastName'] as String? ?? '')),
|
columnSpacing: 24,
|
||||||
DataCell(Text(_roleName(user['role'] as int?))),
|
headingRowColor: WidgetStateProperty.all(Colors.grey.shade50),
|
||||||
DataCell(Row(children: [
|
dividerThickness: 1,
|
||||||
IconButton(
|
columns: const [
|
||||||
icon: Icon(Icons.edit, color: kPrimaryColor),
|
DataColumn(label: Text('Email', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||||
onPressed: () => _showEditDialog(context, managerCtx, user),
|
DataColumn(label: Text('Prénom', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||||
),
|
DataColumn(label: Text('Nom', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||||
IconButton(
|
DataColumn(label: Text('Rôle', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
DataColumn(label: Text('Actions', style: TextStyle(fontWeight: FontWeight.w600))),
|
||||||
onPressed: () => _confirmDelete(context, managerCtx, user),
|
],
|
||||||
),
|
rows: _users.map((user) {
|
||||||
])),
|
final roleColor = _roleColor(user['role']);
|
||||||
]);
|
return DataRow(cells: [
|
||||||
}).toList(),
|
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? _apiKeyApi;
|
||||||
ApiKeyApi? get apiKeyApi => _apiKeyApi;
|
ApiKeyApi? get apiKeyApi => _apiKeyApi;
|
||||||
|
|
||||||
|
NotificationApi? _notificationApi;
|
||||||
|
NotificationApi? get notificationApi => _notificationApi;
|
||||||
|
|
||||||
Client(String path) {
|
Client(String path) {
|
||||||
_apiClient = ApiClient(basePath: path);
|
_apiClient = ApiClient(basePath: path);
|
||||||
//basePath: "https://192.168.31.140");
|
//basePath: "https://192.168.31.140");
|
||||||
@ -66,5 +69,6 @@ class Client {
|
|||||||
_sectionEventApi = SectionEventApi(_apiClient);
|
_sectionEventApi = SectionEventApi(_apiClient);
|
||||||
_statsApi = StatsApi(_apiClient);
|
_statsApi = StatsApi(_apiClient);
|
||||||
_apiKeyApi = ApiKeyApi(_apiClient);
|
_apiKeyApi = ApiKeyApi(_apiClient);
|
||||||
|
_notificationApi = NotificationApi(_apiClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -263,10 +263,15 @@ class _MyAppState extends State<MyApp> {
|
|||||||
//const Locale('fr', 'FR'),
|
//const Locale('fr', 'FR'),
|
||||||
],*/
|
],*/
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
primarySwatch: Colors.blue,
|
|
||||||
primaryColor: kPrimaryColor,
|
primaryColor: kPrimaryColor,
|
||||||
scaffoldBackgroundColor: kBackgroundColor,
|
scaffoldBackgroundColor: kBackgroundColor,
|
||||||
//fontFamily: "Vollkorn",
|
colorScheme: ColorScheme.light(
|
||||||
|
primary: kPrimaryColor,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
secondary: kPrimaryColor,
|
||||||
|
onSecondary: Colors.white,
|
||||||
|
error: kError,
|
||||||
|
),
|
||||||
textTheme: TextTheme(bodyLarge: TextStyle(color: kBodyTextColor)),
|
textTheme: TextTheme(bodyLarge: TextStyle(color: kBodyTextColor)),
|
||||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -41,9 +41,14 @@ part 'api/section_agenda_api.dart';
|
|||||||
part 'api/section_event_api.dart';
|
part 'api/section_event_api.dart';
|
||||||
part 'api/section_map_api.dart';
|
part 'api/section_map_api.dart';
|
||||||
part 'api/section_quiz_api.dart';
|
part 'api/section_quiz_api.dart';
|
||||||
|
part 'api/notification_api.dart';
|
||||||
part 'api/stats_api.dart';
|
part 'api/stats_api.dart';
|
||||||
part 'api/user_api.dart';
|
part 'api/user_api.dart';
|
||||||
|
|
||||||
|
part 'model/push_notification_dto.dart';
|
||||||
|
part 'model/send_notification_request.dart';
|
||||||
|
part 'model/schedule_notification_request.dart';
|
||||||
|
|
||||||
part 'model/agenda_dto.dart';
|
part 'model/agenda_dto.dart';
|
||||||
part 'model/agenda_dto_all_of_agenda_map_provider.dart';
|
part 'model/agenda_dto_all_of_agenda_map_provider.dart';
|
||||||
part 'model/ai_card_dto.dart';
|
part 'model/ai_card_dto.dart';
|
||||||
|
|||||||
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /api/SectionAgenda/{sectionAgendaId}/events/upcoming' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] sectionAgendaId (required):
|
||||||
|
Future<Response> sectionAgendaGetUpcomingEventsWithHttpInfo(
|
||||||
|
String sectionAgendaId,
|
||||||
|
) async {
|
||||||
|
final path = r'/api/SectionAgenda/{sectionAgendaId}/events/upcoming'
|
||||||
|
.replaceAll('{sectionAgendaId}', sectionAgendaId);
|
||||||
|
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] sectionAgendaId (required):
|
||||||
|
Future<List<EventAgendaDTO>?> sectionAgendaGetUpcomingEvents(
|
||||||
|
String sectionAgendaId,
|
||||||
|
) async {
|
||||||
|
final response = await sectionAgendaGetUpcomingEventsWithHttpInfo(sectionAgendaId);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<EventAgendaDTO>') as List)
|
||||||
|
.cast<EventAgendaDTO>()
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'PUT /api/SectionAgenda/event' operation and returns the [Response].
|
/// Performs an HTTP 'PUT /api/SectionAgenda/event' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
@ -662,7 +662,7 @@ class SectionApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
Future<List<Object>?> sectionGetFromConfigurationDetail(
|
Future<dynamic> sectionGetFromConfigurationDetail(
|
||||||
String id,
|
String id,
|
||||||
) async {
|
) async {
|
||||||
final response = await sectionGetFromConfigurationDetailWithHttpInfo(
|
final response = await sectionGetFromConfigurationDetailWithHttpInfo(
|
||||||
@ -671,16 +671,10 @@ class SectionApi {
|
|||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
// Sections are polymorphic — bypass generated deserialization and return raw JSON
|
||||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
|
||||||
// FormatException when trying to decode an empty string.
|
|
||||||
if (response.body.isNotEmpty &&
|
if (response.body.isNotEmpty &&
|
||||||
response.statusCode != HttpStatus.noContent) {
|
response.statusCode != HttpStatus.noContent) {
|
||||||
final responseBody = await _decodeBodyBytes(response);
|
return json.decode(await _decodeBodyBytes(response));
|
||||||
return (await apiClient.deserializeAsync(responseBody, 'List<Object>')
|
|
||||||
as List)
|
|
||||||
.cast<Object>()
|
|
||||||
.toList(growable: false);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -481,4 +481,91 @@ class SectionEventApi {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /api/SectionEvent/{sectionEventId}/global-map-annotations' operation and returns the [Response].
|
||||||
|
Future<Response> sectionEventGetGlobalMapAnnotationsWithHttpInfo(
|
||||||
|
String sectionEventId,
|
||||||
|
) async {
|
||||||
|
final path = r'/api/SectionEvent/{sectionEventId}/global-map-annotations'
|
||||||
|
.replaceAll('{sectionEventId}', sectionEventId);
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
null,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET global map annotations for a section event.
|
||||||
|
Future<List<MapAnnotationDTO>?> sectionEventGetGlobalMapAnnotations(
|
||||||
|
String sectionEventId,
|
||||||
|
) async {
|
||||||
|
final response = await sectionEventGetGlobalMapAnnotationsWithHttpInfo(sectionEventId);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<MapAnnotationDTO>') as List)
|
||||||
|
.cast<MapAnnotationDTO>()
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /api/SectionEvent/{sectionEventId}/global-map-annotations' operation and returns the [Response].
|
||||||
|
Future<Response> sectionEventCreateGlobalMapAnnotationWithHttpInfo(
|
||||||
|
String sectionEventId,
|
||||||
|
MapAnnotationDTO mapAnnotationDTO,
|
||||||
|
) async {
|
||||||
|
final path = r'/api/SectionEvent/{sectionEventId}/global-map-annotations'
|
||||||
|
.replaceAll('{sectionEventId}', sectionEventId);
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
mapAnnotationDTO,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST create a global map annotation on a section event.
|
||||||
|
Future<MapAnnotationDTO?> sectionEventCreateGlobalMapAnnotation(
|
||||||
|
String sectionEventId,
|
||||||
|
MapAnnotationDTO mapAnnotationDTO,
|
||||||
|
) async {
|
||||||
|
final response = await sectionEventCreateGlobalMapAnnotationWithHttpInfo(
|
||||||
|
sectionEventId,
|
||||||
|
mapAnnotationDTO,
|
||||||
|
);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(
|
||||||
|
await _decodeBodyBytes(response),
|
||||||
|
'MapAnnotationDTO',
|
||||||
|
) as MapAnnotationDTO;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ class AiChatResponseNavigation {
|
|||||||
this.sectionId,
|
this.sectionId,
|
||||||
this.sectionTitle,
|
this.sectionTitle,
|
||||||
this.sectionType,
|
this.sectionType,
|
||||||
|
this.imageUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
String? sectionId;
|
String? sectionId;
|
||||||
@ -24,6 +25,8 @@ class AiChatResponseNavigation {
|
|||||||
|
|
||||||
String? sectionType;
|
String? sectionType;
|
||||||
|
|
||||||
|
String? imageUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@ -60,6 +63,11 @@ class AiChatResponseNavigation {
|
|||||||
} else {
|
} else {
|
||||||
json[r'sectionType'] = null;
|
json[r'sectionType'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.imageUrl != null) {
|
||||||
|
json[r'imageUrl'] = this.imageUrl;
|
||||||
|
} else {
|
||||||
|
json[r'imageUrl'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +95,7 @@ class AiChatResponseNavigation {
|
|||||||
sectionId: mapValueOfType<String>(json, r'sectionId'),
|
sectionId: mapValueOfType<String>(json, r'sectionId'),
|
||||||
sectionTitle: mapValueOfType<String>(json, r'sectionTitle'),
|
sectionTitle: mapValueOfType<String>(json, r'sectionTitle'),
|
||||||
sectionType: mapValueOfType<String>(json, r'sectionType'),
|
sectionType: mapValueOfType<String>(json, r'sectionType'),
|
||||||
|
imageUrl: mapValueOfType<String>(json, r'imageUrl'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class CreateApiKeyRequest {
|
|||||||
CreateApiKeyRequest({
|
CreateApiKeyRequest({
|
||||||
this.name,
|
this.name,
|
||||||
this.appType,
|
this.appType,
|
||||||
|
this.dateExpiration,
|
||||||
});
|
});
|
||||||
|
|
||||||
String? name;
|
String? name;
|
||||||
@ -27,21 +28,25 @@ class CreateApiKeyRequest {
|
|||||||
///
|
///
|
||||||
ApiKeyAppType? appType;
|
ApiKeyAppType? appType;
|
||||||
|
|
||||||
|
DateTime? dateExpiration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is CreateApiKeyRequest &&
|
other is CreateApiKeyRequest &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.appType == appType;
|
other.appType == appType &&
|
||||||
|
other.dateExpiration == dateExpiration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(name == null ? 0 : name!.hashCode) +
|
(name == null ? 0 : name!.hashCode) +
|
||||||
(appType == null ? 0 : appType!.hashCode);
|
(appType == null ? 0 : appType!.hashCode) +
|
||||||
|
(dateExpiration == null ? 0 : dateExpiration!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'CreateApiKeyRequest[name=$name, appType=$appType]';
|
String toString() => 'CreateApiKeyRequest[name=$name, appType=$appType, dateExpiration=$dateExpiration]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -55,6 +60,11 @@ class CreateApiKeyRequest {
|
|||||||
} else {
|
} else {
|
||||||
json[r'appType'] = null;
|
json[r'appType'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.dateExpiration != null) {
|
||||||
|
json[r'dateExpiration'] = this.dateExpiration!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
json[r'dateExpiration'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +91,7 @@ class CreateApiKeyRequest {
|
|||||||
return CreateApiKeyRequest(
|
return CreateApiKeyRequest(
|
||||||
name: mapValueOfType<String>(json, r'name'),
|
name: mapValueOfType<String>(json, r'name'),
|
||||||
appType: ApiKeyAppType.fromJson(json[r'appType']),
|
appType: ApiKeyAppType.fromJson(json[r'appType']),
|
||||||
|
dateExpiration: mapDateTime(json, r'dateExpiration', r''),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -28,6 +28,11 @@ class EventAgendaDTO {
|
|||||||
this.email,
|
this.email,
|
||||||
this.sectionAgendaId,
|
this.sectionAgendaId,
|
||||||
this.sectionEventId,
|
this.sectionEventId,
|
||||||
|
this.isSynced,
|
||||||
|
this.idVideoYoutube,
|
||||||
|
this.videoLink,
|
||||||
|
this.videoResourceId,
|
||||||
|
this.videoResource,
|
||||||
});
|
});
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -66,6 +71,16 @@ class EventAgendaDTO {
|
|||||||
|
|
||||||
String? sectionEventId;
|
String? sectionEventId;
|
||||||
|
|
||||||
|
bool? isSynced;
|
||||||
|
|
||||||
|
String? idVideoYoutube;
|
||||||
|
|
||||||
|
String? videoLink;
|
||||||
|
|
||||||
|
String? videoResourceId;
|
||||||
|
|
||||||
|
EventAgendaDTOResource? videoResource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@ -84,7 +99,12 @@ class EventAgendaDTO {
|
|||||||
other.phone == phone &&
|
other.phone == phone &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.sectionAgendaId == sectionAgendaId &&
|
other.sectionAgendaId == sectionAgendaId &&
|
||||||
other.sectionEventId == sectionEventId;
|
other.sectionEventId == sectionEventId &&
|
||||||
|
other.isSynced == isSynced &&
|
||||||
|
other.idVideoYoutube == idVideoYoutube &&
|
||||||
|
other.videoLink == videoLink &&
|
||||||
|
other.videoResourceId == videoResourceId &&
|
||||||
|
other.videoResource == videoResource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -103,7 +123,12 @@ class EventAgendaDTO {
|
|||||||
(phone == null ? 0 : phone!.hashCode) +
|
(phone == null ? 0 : phone!.hashCode) +
|
||||||
(email == null ? 0 : email!.hashCode) +
|
(email == null ? 0 : email!.hashCode) +
|
||||||
(sectionAgendaId == null ? 0 : sectionAgendaId!.hashCode) +
|
(sectionAgendaId == null ? 0 : sectionAgendaId!.hashCode) +
|
||||||
(sectionEventId == null ? 0 : sectionEventId!.hashCode);
|
(sectionEventId == null ? 0 : sectionEventId!.hashCode) +
|
||||||
|
(isSynced == null ? 0 : isSynced!.hashCode) +
|
||||||
|
(idVideoYoutube == null ? 0 : idVideoYoutube!.hashCode) +
|
||||||
|
(videoLink == null ? 0 : videoLink!.hashCode) +
|
||||||
|
(videoResourceId == null ? 0 : videoResourceId!.hashCode) +
|
||||||
|
(videoResource == null ? 0 : videoResource!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
@ -186,6 +211,31 @@ class EventAgendaDTO {
|
|||||||
} else {
|
} else {
|
||||||
json[r'sectionEventId'] = null;
|
json[r'sectionEventId'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.isSynced != null) {
|
||||||
|
json[r'isSynced'] = this.isSynced;
|
||||||
|
} else {
|
||||||
|
json[r'isSynced'] = null;
|
||||||
|
}
|
||||||
|
if (this.idVideoYoutube != null) {
|
||||||
|
json[r'idVideoYoutube'] = this.idVideoYoutube;
|
||||||
|
} else {
|
||||||
|
json[r'idVideoYoutube'] = null;
|
||||||
|
}
|
||||||
|
if (this.videoLink != null) {
|
||||||
|
json[r'videoLink'] = this.videoLink;
|
||||||
|
} else {
|
||||||
|
json[r'videoLink'] = null;
|
||||||
|
}
|
||||||
|
if (this.videoResourceId != null) {
|
||||||
|
json[r'videoResourceId'] = this.videoResourceId;
|
||||||
|
} else {
|
||||||
|
json[r'videoResourceId'] = null;
|
||||||
|
}
|
||||||
|
if (this.videoResource != null) {
|
||||||
|
json[r'videoResource'] = this.videoResource;
|
||||||
|
} else {
|
||||||
|
json[r'videoResource'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,6 +275,11 @@ class EventAgendaDTO {
|
|||||||
email: mapValueOfType<String>(json, r'email'),
|
email: mapValueOfType<String>(json, r'email'),
|
||||||
sectionAgendaId: mapValueOfType<String>(json, r'sectionAgendaId'),
|
sectionAgendaId: mapValueOfType<String>(json, r'sectionAgendaId'),
|
||||||
sectionEventId: mapValueOfType<String>(json, r'sectionEventId'),
|
sectionEventId: mapValueOfType<String>(json, r'sectionEventId'),
|
||||||
|
isSynced: mapValueOfType<bool>(json, r'isSynced'),
|
||||||
|
idVideoYoutube: mapValueOfType<String>(json, r'idVideoYoutube'),
|
||||||
|
videoLink: mapValueOfType<String>(json, r'videoLink'),
|
||||||
|
videoResourceId: mapValueOfType<String>(json, r'videoResourceId'),
|
||||||
|
videoResource: EventAgendaDTOResource.fromJson(json[r'videoResource']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -32,6 +32,7 @@ class MapDTO {
|
|||||||
this.meterZoneGPS,
|
this.meterZoneGPS,
|
||||||
this.isBeacon,
|
this.isBeacon,
|
||||||
this.beaconId,
|
this.beaconId,
|
||||||
|
this.isListViewEnabled,
|
||||||
this.zoom,
|
this.zoom,
|
||||||
this.mapType,
|
this.mapType,
|
||||||
this.mapTypeMapbox,
|
this.mapTypeMapbox,
|
||||||
@ -114,6 +115,8 @@ class MapDTO {
|
|||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
|
bool? isListViewEnabled;
|
||||||
|
|
||||||
int? zoom;
|
int? zoom;
|
||||||
|
|
||||||
MapTypeApp? mapType;
|
MapTypeApp? mapType;
|
||||||
@ -161,6 +164,7 @@ class MapDTO {
|
|||||||
other.meterZoneGPS == meterZoneGPS &&
|
other.meterZoneGPS == meterZoneGPS &&
|
||||||
other.isBeacon == isBeacon &&
|
other.isBeacon == isBeacon &&
|
||||||
other.beaconId == beaconId &&
|
other.beaconId == beaconId &&
|
||||||
|
other.isListViewEnabled == isListViewEnabled &&
|
||||||
other.zoom == zoom &&
|
other.zoom == zoom &&
|
||||||
other.mapType == mapType &&
|
other.mapType == mapType &&
|
||||||
other.mapTypeMapbox == mapTypeMapbox &&
|
other.mapTypeMapbox == mapTypeMapbox &&
|
||||||
@ -196,6 +200,7 @@ class MapDTO {
|
|||||||
(meterZoneGPS == null ? 0 : meterZoneGPS!.hashCode) +
|
(meterZoneGPS == null ? 0 : meterZoneGPS!.hashCode) +
|
||||||
(isBeacon == null ? 0 : isBeacon!.hashCode) +
|
(isBeacon == null ? 0 : isBeacon!.hashCode) +
|
||||||
(beaconId == null ? 0 : beaconId!.hashCode) +
|
(beaconId == null ? 0 : beaconId!.hashCode) +
|
||||||
|
(isListViewEnabled == null ? 0 : isListViewEnabled!.hashCode) +
|
||||||
(zoom == null ? 0 : zoom!.hashCode) +
|
(zoom == null ? 0 : zoom!.hashCode) +
|
||||||
(mapType == null ? 0 : mapType!.hashCode) +
|
(mapType == null ? 0 : mapType!.hashCode) +
|
||||||
(mapTypeMapbox == null ? 0 : mapTypeMapbox!.hashCode) +
|
(mapTypeMapbox == null ? 0 : mapTypeMapbox!.hashCode) +
|
||||||
@ -310,6 +315,11 @@ class MapDTO {
|
|||||||
} else {
|
} else {
|
||||||
json[r'beaconId'] = null;
|
json[r'beaconId'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.isListViewEnabled != null) {
|
||||||
|
json[r'isListViewEnabled'] = this.isListViewEnabled;
|
||||||
|
} else {
|
||||||
|
json[r'isListViewEnabled'] = null;
|
||||||
|
}
|
||||||
if (this.zoom != null) {
|
if (this.zoom != null) {
|
||||||
json[r'zoom'] = this.zoom;
|
json[r'zoom'] = this.zoom;
|
||||||
} else {
|
} else {
|
||||||
@ -413,6 +423,7 @@ class MapDTO {
|
|||||||
meterZoneGPS: mapValueOfType<int>(json, r'meterZoneGPS'),
|
meterZoneGPS: mapValueOfType<int>(json, r'meterZoneGPS'),
|
||||||
isBeacon: mapValueOfType<bool>(json, r'isBeacon'),
|
isBeacon: mapValueOfType<bool>(json, r'isBeacon'),
|
||||||
beaconId: mapValueOfType<int>(json, r'beaconId'),
|
beaconId: mapValueOfType<int>(json, r'beaconId'),
|
||||||
|
isListViewEnabled: mapValueOfType<bool>(json, r'isListViewEnabled'),
|
||||||
zoom: mapValueOfType<int>(json, r'zoom'),
|
zoom: mapValueOfType<int>(json, r'zoom'),
|
||||||
mapType: MapTypeApp.fromJson(json[r'mapType']),
|
mapType: MapTypeApp.fromJson(json[r'mapType']),
|
||||||
mapTypeMapbox: MapTypeMapBox.fromJson(json[r'mapTypeMapbox']),
|
mapTypeMapbox: MapTypeMapBox.fromJson(json[r'mapTypeMapbox']),
|
||||||
|
|||||||
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.beaconId,
|
||||||
this.startDate,
|
this.startDate,
|
||||||
this.endDate,
|
this.endDate,
|
||||||
|
this.baseSectionMapId,
|
||||||
this.parcoursIds = const [],
|
this.parcoursIds = const [],
|
||||||
|
this.globalMapAnnotations = const [],
|
||||||
this.programme = const [],
|
this.programme = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -116,8 +118,12 @@ class SectionEventDTO {
|
|||||||
///
|
///
|
||||||
DateTime? endDate;
|
DateTime? endDate;
|
||||||
|
|
||||||
|
String? baseSectionMapId;
|
||||||
|
|
||||||
List<String>? parcoursIds;
|
List<String>? parcoursIds;
|
||||||
|
|
||||||
|
List<MapAnnotationDTO>? globalMapAnnotations;
|
||||||
|
|
||||||
List<ProgrammeBlock>? programme;
|
List<ProgrammeBlock>? programme;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -145,7 +151,9 @@ class SectionEventDTO {
|
|||||||
other.beaconId == beaconId &&
|
other.beaconId == beaconId &&
|
||||||
other.startDate == startDate &&
|
other.startDate == startDate &&
|
||||||
other.endDate == endDate &&
|
other.endDate == endDate &&
|
||||||
|
other.baseSectionMapId == baseSectionMapId &&
|
||||||
_deepEquality.equals(other.parcoursIds, parcoursIds) &&
|
_deepEquality.equals(other.parcoursIds, parcoursIds) &&
|
||||||
|
_deepEquality.equals(other.globalMapAnnotations, globalMapAnnotations) &&
|
||||||
_deepEquality.equals(other.programme, programme);
|
_deepEquality.equals(other.programme, programme);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -172,7 +180,9 @@ class SectionEventDTO {
|
|||||||
(beaconId == null ? 0 : beaconId!.hashCode) +
|
(beaconId == null ? 0 : beaconId!.hashCode) +
|
||||||
(startDate == null ? 0 : startDate!.hashCode) +
|
(startDate == null ? 0 : startDate!.hashCode) +
|
||||||
(endDate == null ? 0 : endDate!.hashCode) +
|
(endDate == null ? 0 : endDate!.hashCode) +
|
||||||
|
(baseSectionMapId == null ? 0 : baseSectionMapId!.hashCode) +
|
||||||
(parcoursIds == null ? 0 : parcoursIds!.hashCode) +
|
(parcoursIds == null ? 0 : parcoursIds!.hashCode) +
|
||||||
|
(globalMapAnnotations == null ? 0 : globalMapAnnotations!.hashCode) +
|
||||||
(programme == null ? 0 : programme!.hashCode);
|
(programme == null ? 0 : programme!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -286,11 +296,21 @@ class SectionEventDTO {
|
|||||||
} else {
|
} else {
|
||||||
json[r'endDate'] = null;
|
json[r'endDate'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.baseSectionMapId != null) {
|
||||||
|
json[r'baseSectionMapId'] = this.baseSectionMapId;
|
||||||
|
} else {
|
||||||
|
json[r'baseSectionMapId'] = null;
|
||||||
|
}
|
||||||
if (this.parcoursIds != null) {
|
if (this.parcoursIds != null) {
|
||||||
json[r'parcoursIds'] = this.parcoursIds;
|
json[r'parcoursIds'] = this.parcoursIds;
|
||||||
} else {
|
} else {
|
||||||
json[r'parcoursIds'] = null;
|
json[r'parcoursIds'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.globalMapAnnotations != null) {
|
||||||
|
json[r'globalMapAnnotations'] = this.globalMapAnnotations;
|
||||||
|
} else {
|
||||||
|
json[r'globalMapAnnotations'] = null;
|
||||||
|
}
|
||||||
if (this.programme != null) {
|
if (this.programme != null) {
|
||||||
json[r'programme'] = this.programme;
|
json[r'programme'] = this.programme;
|
||||||
} else {
|
} else {
|
||||||
@ -341,11 +361,13 @@ class SectionEventDTO {
|
|||||||
beaconId: mapValueOfType<int>(json, r'beaconId'),
|
beaconId: mapValueOfType<int>(json, r'beaconId'),
|
||||||
startDate: mapDateTime(json, r'startDate', r''),
|
startDate: mapDateTime(json, r'startDate', r''),
|
||||||
endDate: mapDateTime(json, r'endDate', r''),
|
endDate: mapDateTime(json, r'endDate', r''),
|
||||||
|
baseSectionMapId: mapValueOfType<String>(json, r'baseSectionMapId'),
|
||||||
parcoursIds: json[r'parcoursIds'] is Iterable
|
parcoursIds: json[r'parcoursIds'] is Iterable
|
||||||
? (json[r'parcoursIds'] as Iterable)
|
? (json[r'parcoursIds'] as Iterable)
|
||||||
.cast<String>()
|
.cast<String>()
|
||||||
.toList(growable: false)
|
.toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
|
globalMapAnnotations: MapAnnotationDTO.listFromJson(json[r'globalMapAnnotations']),
|
||||||
programme: ProgrammeBlock.listFromJson(json[r'programme']),
|
programme: ProgrammeBlock.listFromJson(json[r'programme']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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.topAgendaEvents = const [],
|
||||||
this.quizStats = const [],
|
this.quizStats = const [],
|
||||||
this.gameStats = const [],
|
this.gameStats = const [],
|
||||||
});
|
this.topArticles = const [],
|
||||||
|
this.topMenuItems = const [],
|
||||||
|
QrScanStatDTO? qrScans,
|
||||||
|
}) : qrScans = qrScans ?? QrScanStatDTO();
|
||||||
|
|
||||||
int totalSessions;
|
int totalSessions;
|
||||||
int avgVisitDurationSeconds;
|
int avgVisitDurationSeconds;
|
||||||
@ -31,6 +34,9 @@ class StatsSummaryDTO {
|
|||||||
List<AgendaEventStatDTO> topAgendaEvents;
|
List<AgendaEventStatDTO> topAgendaEvents;
|
||||||
List<QuizStatDTO> quizStats;
|
List<QuizStatDTO> quizStats;
|
||||||
List<GameStatDTO> gameStats;
|
List<GameStatDTO> gameStats;
|
||||||
|
List<ArticleStatDTO> topArticles;
|
||||||
|
List<MenuItemStatDTO> topMenuItems;
|
||||||
|
QrScanStatDTO qrScans;
|
||||||
|
|
||||||
static StatsSummaryDTO? fromJson(dynamic value) {
|
static StatsSummaryDTO? fromJson(dynamic value) {
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
@ -50,6 +56,9 @@ class StatsSummaryDTO {
|
|||||||
topAgendaEvents: AgendaEventStatDTO.listFromJson(json[r'topAgendaEvents']),
|
topAgendaEvents: AgendaEventStatDTO.listFromJson(json[r'topAgendaEvents']),
|
||||||
quizStats: QuizStatDTO.listFromJson(json[r'quizStats']),
|
quizStats: QuizStatDTO.listFromJson(json[r'quizStats']),
|
||||||
gameStats: GameStatDTO.listFromJson(json[r'gameStats']),
|
gameStats: GameStatDTO.listFromJson(json[r'gameStats']),
|
||||||
|
topArticles: ArticleStatDTO.listFromJson(json[r'topArticles']),
|
||||||
|
topMenuItems: MenuItemStatDTO.listFromJson(json[r'topMenuItems']),
|
||||||
|
qrScans: QrScanStatDTO.fromJson(json[r'qrScans']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -267,3 +276,83 @@ class GameStatDTO {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ArticleStatDTO {
|
||||||
|
ArticleStatDTO({this.sectionId, this.reads = 0});
|
||||||
|
|
||||||
|
String? sectionId;
|
||||||
|
int reads;
|
||||||
|
|
||||||
|
static ArticleStatDTO? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
return ArticleStatDTO(
|
||||||
|
sectionId: mapValueOfType<String>(json, r'sectionId'),
|
||||||
|
reads: mapValueOfType<int>(json, r'reads') ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ArticleStatDTO> listFromJson(dynamic json) {
|
||||||
|
final result = <ArticleStatDTO>[];
|
||||||
|
if (json is List) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = ArticleStatDTO.fromJson(row);
|
||||||
|
if (value != null) result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuItemStatDTO {
|
||||||
|
MenuItemStatDTO({this.targetSectionId, this.menuItemTitle, this.taps = 0});
|
||||||
|
|
||||||
|
String? targetSectionId;
|
||||||
|
String? menuItemTitle;
|
||||||
|
int taps;
|
||||||
|
|
||||||
|
static MenuItemStatDTO? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
return MenuItemStatDTO(
|
||||||
|
targetSectionId: mapValueOfType<String>(json, r'targetSectionId'),
|
||||||
|
menuItemTitle: mapValueOfType<String>(json, r'menuItemTitle'),
|
||||||
|
taps: mapValueOfType<int>(json, r'taps') ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MenuItemStatDTO> listFromJson(dynamic json) {
|
||||||
|
final result = <MenuItemStatDTO>[];
|
||||||
|
if (json is List) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = MenuItemStatDTO.fromJson(row);
|
||||||
|
if (value != null) result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QrScanStatDTO {
|
||||||
|
QrScanStatDTO({this.totalScans = 0, this.validScans = 0, this.invalidScans = 0});
|
||||||
|
|
||||||
|
int totalScans;
|
||||||
|
int validScans;
|
||||||
|
int invalidScans;
|
||||||
|
|
||||||
|
static QrScanStatDTO fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
return QrScanStatDTO(
|
||||||
|
totalScans: mapValueOfType<int>(json, r'totalScans') ?? 0,
|
||||||
|
validScans: mapValueOfType<int>(json, r'validScans') ?? 0,
|
||||||
|
invalidScans: mapValueOfType<int>(json, r'invalidScans') ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return QrScanStatDTO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -232,6 +232,7 @@ class VisitEventType {
|
|||||||
static const agendaEventTap = 'AgendaEventTap';
|
static const agendaEventTap = 'AgendaEventTap';
|
||||||
static const menuItemTap = 'MenuItemTap';
|
static const menuItemTap = 'MenuItemTap';
|
||||||
static const assistantMessage = 'AssistantMessage';
|
static const assistantMessage = 'AssistantMessage';
|
||||||
|
static const articleRead = 'ArticleRead';
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{};
|
static const requiredKeys = <String>{};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user