To test - role and switch instance
This commit is contained in:
parent
e8d899310e
commit
b7bd8588fe
@ -15,6 +15,7 @@ class ManagerAppContext with ChangeNotifier{
|
|||||||
ConfigurationDTO? selectedConfiguration;
|
ConfigurationDTO? selectedConfiguration;
|
||||||
SectionDTO? selectedSection;
|
SectionDTO? selectedSection;
|
||||||
bool? isLoading = false;
|
bool? isLoading = false;
|
||||||
|
UserRole? role;
|
||||||
|
|
||||||
ManagerAppContext({this.email, this.accessToken});
|
ManagerAppContext({this.email, this.accessToken});
|
||||||
|
|
||||||
|
|||||||
@ -5,17 +5,19 @@ class Session {
|
|||||||
String? token;
|
String? token;
|
||||||
String? password;
|
String? password;
|
||||||
String? instanceId;
|
String? instanceId;
|
||||||
|
int? role;
|
||||||
|
|
||||||
Session({required this.rememberMe, this.host, this.email, this.token, this.password, this.instanceId});
|
Session({required this.rememberMe, this.host, this.email, this.token, this.password, this.instanceId, this.role});
|
||||||
|
|
||||||
factory Session.fromJson(Map<String, dynamic> json) {
|
factory Session.fromJson(Map<String, dynamic> json) {
|
||||||
return new Session(
|
return new Session(
|
||||||
rememberMe: json['rememberMe'] as bool,
|
rememberMe: json['rememberMe'] as bool,
|
||||||
host: json['host'] as String,
|
host: json['host'] as String?,
|
||||||
email: json['email'] as String,
|
email: json['email'] as String?,
|
||||||
token: json['token'] as String,
|
token: json['token'] as String?,
|
||||||
password: json['password'] as String,
|
password: json['password'] as String?,
|
||||||
instanceId: json['instanceId'] as String,
|
instanceId: json['instanceId'] as String?,
|
||||||
|
role: json['role'] as int?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,12 +28,13 @@ class Session {
|
|||||||
'email': email,
|
'email': email,
|
||||||
'token': token,
|
'token': token,
|
||||||
'password': password,
|
'password': password,
|
||||||
'instanceId': instanceId
|
'instanceId': instanceId,
|
||||||
|
'role': role,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '{rememberMe: $rememberMe, host: $host, email: $email, token: $token, password: $password, instanceId: $instanceId}';
|
return '{rememberMe: $rememberMe, host: $host, email: $email, token: $token, password: $password, instanceId: $instanceId, role: $role}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
246
lib/Screens/ApiKeys/api_keys_screen.dart
Normal file
246
lib/Screens/ApiKeys/api_keys_screen.dart
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.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 ApiKeysScreen extends StatefulWidget {
|
||||||
|
const ApiKeysScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ApiKeysScreenState createState() => _ApiKeysScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ApiKeysScreenState extends State<ApiKeysScreen> {
|
||||||
|
List<Map<String, dynamic>> _keys = [];
|
||||||
|
bool _loading = true;
|
||||||
|
|
||||||
|
static String _appTypeName(int? v) {
|
||||||
|
switch (v) {
|
||||||
|
case 0: return 'VisitApp';
|
||||||
|
case 1: return 'TabletApp';
|
||||||
|
default: return 'Other';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadKeys(ManagerAppContext ctx) async {
|
||||||
|
try {
|
||||||
|
final response = await ctx.clientAPI!.apiKeyApi!.apiKeyGetApiKeysWithHttpInfo();
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final List<dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
setState(() {
|
||||||
|
_keys = json.cast<Map<String, dynamic>>();
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _loading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _createKey(ManagerAppContext ctx, String name, ApiKeyAppType appType) async {
|
||||||
|
final response = await ctx.clientAPI!.apiKeyApi!.apiKeyCreateApiKeyWithHttpInfo(
|
||||||
|
CreateApiKeyRequest(name: name, appType: appType),
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
final Map<String, dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
await _loadKeys(ctx);
|
||||||
|
return json['key'] as String?;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _revokeKey(ManagerAppContext ctx, String id) async {
|
||||||
|
await ctx.clientAPI!.apiKeyApi!.apiKeyRevokeApiKey(id);
|
||||||
|
await _loadKeys(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
|
||||||
|
final nameCtrl = TextEditingController();
|
||||||
|
ApiKeyAppType selectedType = ApiKeyAppType.number0;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StatefulBuilder(builder: (ctx2, setLocal) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Créer une clé API'),
|
||||||
|
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
TextField(controller: nameCtrl, decoration: const InputDecoration(labelText: 'Nom')),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Type d\'application', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
DropdownButton<ApiKeyAppType>(
|
||||||
|
value: selectedType,
|
||||||
|
isExpanded: true,
|
||||||
|
items: ApiKeyAppType.values
|
||||||
|
.map((t) => DropdownMenuItem(value: t, child: Text(_appTypeName(t.value))))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) => setLocal(() => selectedType = v!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx2), child: const Text('Annuler')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(ctx2);
|
||||||
|
final plainKey = await _createKey(ctx, nameCtrl.text, selectedType);
|
||||||
|
if (plainKey != null && context.mounted) {
|
||||||
|
_showPlainKeyDialog(context, plainKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Créer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showPlainKeyDialog(BuildContext context, String plainKey) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('Clé API créée'),
|
||||||
|
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
const Text(
|
||||||
|
'Copiez cette clé maintenant — elle ne sera plus affichée.',
|
||||||
|
style: TextStyle(color: Colors.orange, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(child: SelectableText(plainKey, style: const TextStyle(fontFamily: 'monospace', fontSize: 13))),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.copy, size: 18),
|
||||||
|
tooltip: 'Copier',
|
||||||
|
onPressed: () => Clipboard.setData(ClipboardData(text: plainKey)),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
actions: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('J\'ai copié la clé'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmRevoke(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> key) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('Révoquer la clé API'),
|
||||||
|
content: Text('Révoquer « ${key['name']} » ? Les apps utilisant cette clé perdront l\'accès.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Annuler')),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await _revokeKey(ctx, key['id'] as String);
|
||||||
|
},
|
||||||
|
child: const Text('Révoquer', style: TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final ctx = Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
|
||||||
|
_loadKeys(ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final appContext = Provider.of<AppContext>(context);
|
||||||
|
final managerCtx = appContext.getContext() as ManagerAppContext;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Clés API', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: kPrimaryColor)),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => _showCreateDialog(context, managerCtx),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Créer une clé'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (_loading)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else if (_keys.isEmpty)
|
||||||
|
const Center(child: Text('Aucune clé API'))
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: DataTable(
|
||||||
|
columns: const [
|
||||||
|
DataColumn(label: Text('Nom')),
|
||||||
|
DataColumn(label: Text('Type')),
|
||||||
|
DataColumn(label: Text('Créée le')),
|
||||||
|
DataColumn(label: Text('Statut')),
|
||||||
|
DataColumn(label: Text('Actions')),
|
||||||
|
],
|
||||||
|
rows: _keys.map((key) {
|
||||||
|
final isActive = key['isActive'] as bool? ?? false;
|
||||||
|
final dateRaw = key['dateCreation'] as String?;
|
||||||
|
final date = dateRaw != null
|
||||||
|
? DateTime.tryParse(dateRaw)?.toLocal()
|
||||||
|
: null;
|
||||||
|
final dateStr = date != null
|
||||||
|
? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'
|
||||||
|
: '—';
|
||||||
|
|
||||||
|
return DataRow(cells: [
|
||||||
|
DataCell(Text(key['name'] as String? ?? '')),
|
||||||
|
DataCell(Text(_appTypeName(key['appType'] as int?))),
|
||||||
|
DataCell(Text(dateStr)),
|
||||||
|
DataCell(Chip(
|
||||||
|
label: Text(isActive ? 'Active' : 'Révoquée',
|
||||||
|
style: TextStyle(color: isActive ? Colors.green.shade700 : Colors.red.shade700, fontSize: 12)),
|
||||||
|
backgroundColor: isActive ? Colors.green.shade50 : Colors.red.shade50,
|
||||||
|
side: BorderSide(color: isActive ? Colors.green.shade200 : Colors.red.shade200),
|
||||||
|
)),
|
||||||
|
DataCell(isActive
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.block, color: Colors.red),
|
||||||
|
tooltip: 'Révoquer',
|
||||||
|
onPressed: () => _confirmRevoke(context, managerCtx, key),
|
||||||
|
)
|
||||||
|
: const SizedBox()),
|
||||||
|
]);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,11 +8,13 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:manager_app/Models/managerContext.dart';
|
import 'package:manager_app/Models/managerContext.dart';
|
||||||
import 'package:manager_app/Models/menu.dart';
|
import 'package:manager_app/Models/menu.dart';
|
||||||
import 'package:manager_app/Models/menuSection.dart';
|
import 'package:manager_app/Models/menuSection.dart';
|
||||||
|
import 'package:manager_app/Screens/ApiKeys/api_keys_screen.dart';
|
||||||
import 'package:manager_app/Screens/Configurations/configurations_screen.dart';
|
import 'package:manager_app/Screens/Configurations/configurations_screen.dart';
|
||||||
import 'package:manager_app/Screens/Kiosk_devices/kiosk_screen.dart';
|
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/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';
|
||||||
import 'package:manager_app/main.dart';
|
import 'package:manager_app/main.dart';
|
||||||
@ -55,6 +57,7 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
menu.sections!.add(MenuSection(name: "Statistiques", type: "statistics", menuId: 7, subMenu: []));
|
menu.sections!.add(MenuSection(name: "Statistiques", type: "statistics", menuId: 7, subMenu: []));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if(currentPosition.value == null) {
|
if(currentPosition.value == null) {
|
||||||
if (widget.instance.isMobile!) {
|
if (widget.instance.isMobile!) {
|
||||||
currentPosition.value = 1;
|
currentPosition.value = 1;
|
||||||
@ -72,6 +75,72 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
|
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showSwitchInstanceDialog(BuildContext context, ManagerAppContext managerCtx) async {
|
||||||
|
List<Instance>? instances;
|
||||||
|
try {
|
||||||
|
instances = await managerCtx.clientAPI!.instanceApi!.instanceGet();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
final instanceList = instances ?? [];
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('Changer d\'instance'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: instanceList.isEmpty
|
||||||
|
? const Text('Aucune instance trouvée')
|
||||||
|
: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: instanceList.length,
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final inst = instanceList[i];
|
||||||
|
final isCurrent = inst.id == managerCtx.instanceId;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.business,
|
||||||
|
color: isCurrent ? kPrimaryColor : kBodyTextColor,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
inst.name,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isCurrent ? kPrimaryColor : kBodyTextColor,
|
||||||
|
fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: isCurrent ? const Icon(Icons.check, color: Colors.green) : null,
|
||||||
|
onTap: isCurrent
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
final newInstance = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id);
|
||||||
|
if (newInstance != null && context.mounted) {
|
||||||
|
setState(() {
|
||||||
|
managerCtx.instanceId = newInstance.id;
|
||||||
|
managerCtx.instanceDTO = newInstance;
|
||||||
|
menu.sections = [];
|
||||||
|
});
|
||||||
|
final view = newInstance.isMobile! ? 'mobile'
|
||||||
|
: newInstance.isTablet! ? 'kiosk'
|
||||||
|
: newInstance.isWeb! ? 'web'
|
||||||
|
: 'vr';
|
||||||
|
context.go('/main/$view');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Fermer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@ -196,7 +265,7 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Footer: Email + Logout button
|
// Footer: Email + Switch instance (SuperAdmin) + Logout
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -206,6 +275,12 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
style: TextStyle(color: kBodyTextColor, fontSize: 16, fontWeight: FontWeight.w300, fontFamily: "Helvetica"),
|
style: TextStyle(color: kBodyTextColor, fontSize: 16, fontWeight: FontWeight.w300, fontFamily: "Helvetica"),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
|
if ((appContext.getContext() as ManagerAppContext).role == UserRole.SuperAdmin)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.swap_horiz, color: kPrimaryColor),
|
||||||
|
tooltip: 'Changer d\'instance',
|
||||||
|
onPressed: () => _showSwitchInstanceDialog(context, appContext.getContext() as ManagerAppContext),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.logout, color: kPrimaryColor),
|
icon: Icon(Icons.logout, color: kPrimaryColor),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@ -238,6 +313,16 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
final appContext = Provider.of<AppContext>(context);
|
final appContext = Provider.of<AppContext>(context);
|
||||||
ManagerAppContext managerAppContext = appContext.getContext();
|
ManagerAppContext managerAppContext = appContext.getContext();
|
||||||
|
|
||||||
|
// Synchronise les items de menu sensibles au rôle à chaque rebuild
|
||||||
|
final role = managerAppContext.role;
|
||||||
|
final hasAdminItems = menu.sections!.any((s) => s.menuId == 8);
|
||||||
|
if (role != null && role.value <= 1 && !hasAdminItems) {
|
||||||
|
menu.sections!.add(MenuSection(name: "Utilisateurs", type: "users", menuId: 8, subMenu: []));
|
||||||
|
menu.sections!.add(MenuSection(name: "Clés API", type: "apikeys", menuId: 9, subMenu: []));
|
||||||
|
} else if ((role == null || role.value > 1) && hasAdminItems) {
|
||||||
|
menu.sections!.removeWhere((s) => s.menuId == 8 || s.menuId == 9);
|
||||||
|
}
|
||||||
|
|
||||||
Size size = MediaQuery.of(context).size;
|
Size size = MediaQuery.of(context).size;
|
||||||
bool isMobile = size.width < 850;
|
bool isMobile = size.width < 850;
|
||||||
|
|
||||||
@ -312,6 +397,12 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
case "statistics":
|
case "statistics":
|
||||||
currentPosition = 7;
|
currentPosition = 7;
|
||||||
break;
|
break;
|
||||||
|
case "users":
|
||||||
|
currentPosition = 8;
|
||||||
|
break;
|
||||||
|
case "apikeys":
|
||||||
|
currentPosition = 9;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,6 +463,16 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
child: StatisticsScreen()
|
child: StatisticsScreen()
|
||||||
);
|
);
|
||||||
|
case 'users':
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: UsersScreen()
|
||||||
|
);
|
||||||
|
case 'apikeys':
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: ApiKeysScreen()
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return Text('Hellow default');
|
return Text('Hellow default');
|
||||||
}
|
}
|
||||||
|
|||||||
270
lib/Screens/Users/users_screen.dart
Normal file
270
lib/Screens/Users/users_screen.dart
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
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:provider/provider.dart';
|
||||||
|
|
||||||
|
class UsersScreen extends StatefulWidget {
|
||||||
|
const UsersScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_UsersScreenState createState() => _UsersScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UsersScreenState extends State<UsersScreen> {
|
||||||
|
List<Map<String, dynamic>> _users = [];
|
||||||
|
bool _loading = true;
|
||||||
|
|
||||||
|
static String _roleName(int? v) {
|
||||||
|
switch (v) {
|
||||||
|
case 0: return 'SuperAdmin';
|
||||||
|
case 1: return 'InstanceAdmin';
|
||||||
|
case 2: return 'ContentEditor';
|
||||||
|
case 3: return 'Viewer';
|
||||||
|
default: return '—';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roles the caller is allowed to assign (can't assign higher than own role)
|
||||||
|
List<int> _allowedRoles(int callerRoleValue) =>
|
||||||
|
[0, 1, 2, 3].where((r) => r >= callerRoleValue).toList();
|
||||||
|
|
||||||
|
Future<void> _loadUsers(ManagerAppContext ctx) async {
|
||||||
|
try {
|
||||||
|
final response = await ctx.clientAPI!.userApi!.userGetWithHttpInfo();
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final List<dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
setState(() {
|
||||||
|
_users = json.cast<Map<String, dynamic>>();
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _loading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createUser(ManagerAppContext ctx, String email,
|
||||||
|
String firstName, String lastName, String password, int roleValue) async {
|
||||||
|
final body = {
|
||||||
|
'email': email,
|
||||||
|
'firstName': firstName,
|
||||||
|
'lastName': lastName,
|
||||||
|
'password': password,
|
||||||
|
'role': roleValue,
|
||||||
|
};
|
||||||
|
await ctx.clientAPI!.apiApi!.invokeAPI(
|
||||||
|
'/api/User', 'POST', [], body, {}, {}, 'application/json');
|
||||||
|
await _loadUsers(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateUser(ManagerAppContext ctx, String id, String firstName,
|
||||||
|
String lastName, int roleValue) async {
|
||||||
|
final body = {
|
||||||
|
'id': id,
|
||||||
|
'firstName': firstName,
|
||||||
|
'lastName': lastName,
|
||||||
|
'role': roleValue,
|
||||||
|
};
|
||||||
|
await ctx.clientAPI!.apiApi!.invokeAPI(
|
||||||
|
'/api/User', 'PUT', [], body, {}, {}, 'application/json');
|
||||||
|
await _loadUsers(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteUser(ManagerAppContext ctx, String id) async {
|
||||||
|
await ctx.clientAPI!.userApi!.userDeleteUser(id);
|
||||||
|
await _loadUsers(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
|
||||||
|
final callerRole = ctx.role?.value ?? 2;
|
||||||
|
final emailCtrl = TextEditingController();
|
||||||
|
final firstCtrl = TextEditingController();
|
||||||
|
final lastCtrl = TextEditingController();
|
||||||
|
final passCtrl = TextEditingController();
|
||||||
|
int selectedRole = callerRole; // default: same as caller
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StatefulBuilder(builder: (ctx2, setLocal) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Créer un utilisateur'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
TextField(controller: emailCtrl, decoration: const InputDecoration(labelText: 'Email')),
|
||||||
|
TextField(controller: firstCtrl, decoration: const InputDecoration(labelText: 'Prénom')),
|
||||||
|
TextField(controller: lastCtrl, decoration: const InputDecoration(labelText: 'Nom')),
|
||||||
|
TextField(controller: passCtrl, obscureText: true, decoration: const InputDecoration(labelText: 'Mot de passe')),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Rôle', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
DropdownButton<int>(
|
||||||
|
value: selectedRole,
|
||||||
|
isExpanded: true,
|
||||||
|
items: _allowedRoles(callerRole)
|
||||||
|
.map((r) => DropdownMenuItem(value: r, child: Text(_roleName(r))))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) => setLocal(() => selectedRole = v!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx2), child: const Text('Annuler')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(ctx2);
|
||||||
|
await _createUser(ctx, emailCtrl.text, firstCtrl.text,
|
||||||
|
lastCtrl.text, passCtrl.text, selectedRole);
|
||||||
|
},
|
||||||
|
child: const Text('Créer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showEditDialog(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> user) {
|
||||||
|
final callerRole = ctx.role?.value ?? 2;
|
||||||
|
final firstCtrl = TextEditingController(text: user['firstName'] as String? ?? '');
|
||||||
|
final lastCtrl = TextEditingController(text: user['lastName'] as String? ?? '');
|
||||||
|
int selectedRole = (user['role'] as int?) ?? callerRole;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StatefulBuilder(builder: (ctx2, setLocal) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Modifier l\'utilisateur'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
TextField(controller: firstCtrl, decoration: const InputDecoration(labelText: 'Prénom')),
|
||||||
|
TextField(controller: lastCtrl, decoration: const InputDecoration(labelText: 'Nom')),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Rôle', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
DropdownButton<int>(
|
||||||
|
value: selectedRole,
|
||||||
|
isExpanded: true,
|
||||||
|
items: _allowedRoles(callerRole)
|
||||||
|
.map((r) => DropdownMenuItem(value: r, child: Text(_roleName(r))))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) => setLocal(() => selectedRole = v!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx2), child: const Text('Annuler')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(ctx2);
|
||||||
|
await _updateUser(ctx, user['id'] as String, firstCtrl.text,
|
||||||
|
lastCtrl.text, selectedRole);
|
||||||
|
},
|
||||||
|
child: const Text('Enregistrer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmDelete(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> user) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('Supprimer l\'utilisateur'),
|
||||||
|
content: Text('Supprimer ${user['email']} ?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Annuler')),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await _deleteUser(ctx, user['id'] as String);
|
||||||
|
},
|
||||||
|
child: const Text('Supprimer', style: TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final ctx = Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
|
||||||
|
_loadUsers(ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final appContext = Provider.of<AppContext>(context);
|
||||||
|
final managerCtx = appContext.getContext() as ManagerAppContext;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Utilisateurs', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: kPrimaryColor)),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => _showCreateDialog(context, managerCtx),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Créer un utilisateur'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (_loading)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else if (_users.isEmpty)
|
||||||
|
const Center(child: Text('Aucun utilisateur'))
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: DataTable(
|
||||||
|
columns: const [
|
||||||
|
DataColumn(label: Text('Email')),
|
||||||
|
DataColumn(label: Text('Prénom')),
|
||||||
|
DataColumn(label: Text('Nom')),
|
||||||
|
DataColumn(label: Text('Rôle')),
|
||||||
|
DataColumn(label: Text('Actions')),
|
||||||
|
],
|
||||||
|
rows: _users.map((user) {
|
||||||
|
return DataRow(cells: [
|
||||||
|
DataCell(Text(user['email'] as String? ?? '')),
|
||||||
|
DataCell(Text(user['firstName'] as String? ?? '')),
|
||||||
|
DataCell(Text(user['lastName'] as String? ?? '')),
|
||||||
|
DataCell(Text(_roleName(user['role'] as int?))),
|
||||||
|
DataCell(Row(children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.edit, color: kPrimaryColor),
|
||||||
|
onPressed: () => _showEditDialog(context, managerCtx, user),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete, color: Colors.red),
|
||||||
|
onPressed: () => _confirmDelete(context, managerCtx, user),
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
]);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -67,6 +67,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
var accessToken = this.token;
|
var accessToken = this.token;
|
||||||
var instanceId = this.instanceId;
|
var instanceId = this.instanceId;
|
||||||
var pinCode = this.pinCode;
|
var pinCode = this.pinCode;
|
||||||
|
UserRole? userRole;
|
||||||
/*print("accessToken");
|
/*print("accessToken");
|
||||||
print(accessToken);*/
|
print(accessToken);*/
|
||||||
if(accessToken == null) {
|
if(accessToken == null) {
|
||||||
@ -84,6 +85,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
accessToken = token.accessToken!;
|
accessToken = token.accessToken!;
|
||||||
instanceId = token.instanceId!;
|
instanceId = token.instanceId!;
|
||||||
pinCode = token.pinCode;
|
pinCode = token.pinCode;
|
||||||
|
userRole = token.role;
|
||||||
|
|
||||||
showNotification(
|
showNotification(
|
||||||
kSuccess, kWhite, 'Connexion réussie', context, null);
|
kSuccess, kWhite, 'Connexion réussie', context, null);
|
||||||
@ -122,13 +124,14 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
managerAppContext.instanceId = instanceId;
|
managerAppContext.instanceId = instanceId;
|
||||||
managerAppContext.pinCode = pinCode;
|
managerAppContext.pinCode = pinCode;
|
||||||
managerAppContext.accessToken = accessToken;
|
managerAppContext.accessToken = accessToken;
|
||||||
|
managerAppContext.role = userRole;
|
||||||
managerAppContext.clientAPI = clientAPI;
|
managerAppContext.clientAPI = clientAPI;
|
||||||
setAccessToken(accessToken);
|
setAccessToken(accessToken);
|
||||||
|
|
||||||
InstanceDTO? instance = await managerAppContext.clientAPI!.instanceApi!.instanceGetDetail(managerAppContext.instanceId!);
|
InstanceDTO? instance = await managerAppContext.clientAPI!.instanceApi!.instanceGetDetail(managerAppContext.instanceId!);
|
||||||
managerAppContext.instanceDTO = instance;
|
managerAppContext.instanceDTO = instance;
|
||||||
|
|
||||||
FileHelper().writeSession(new Session(rememberMe: true, instanceId: instanceId, host: host, token: accessToken, password: password, email: email));
|
FileHelper().writeSession(new Session(rememberMe: true, instanceId: instanceId, host: host, token: accessToken, password: password, email: email, role: userRole?.value));
|
||||||
appContext.setContext(managerAppContext);
|
appContext.setContext(managerAppContext);
|
||||||
|
|
||||||
if(instance!.isMobile!) {
|
if(instance!.isMobile!) {
|
||||||
|
|||||||
@ -44,6 +44,9 @@ class Client {
|
|||||||
StatsApi? _statsApi;
|
StatsApi? _statsApi;
|
||||||
StatsApi? get statsApi => _statsApi;
|
StatsApi? get statsApi => _statsApi;
|
||||||
|
|
||||||
|
ApiKeyApi? _apiKeyApi;
|
||||||
|
ApiKeyApi? get apiKeyApi => _apiKeyApi;
|
||||||
|
|
||||||
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");
|
||||||
@ -62,5 +65,6 @@ class Client {
|
|||||||
_sectionAgendaApi = SectionAgendaApi(_apiClient);
|
_sectionAgendaApi = SectionAgendaApi(_apiClient);
|
||||||
_sectionEventApi = SectionEventApi(_apiClient);
|
_sectionEventApi = SectionEventApi(_apiClient);
|
||||||
_statsApi = StatsApi(_apiClient);
|
_statsApi = StatsApi(_apiClient);
|
||||||
|
_apiKeyApi = ApiKeyApi(_apiClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -193,6 +193,7 @@ Future<InstanceDTO?> getInstanceInfo(ManagerAppContext managerAppContext) async
|
|||||||
managerAppContext.host = managerAppContext.host ?? session.host;
|
managerAppContext.host = managerAppContext.host ?? session.host;
|
||||||
managerAppContext.accessToken = managerAppContext.accessToken ?? session.token;
|
managerAppContext.accessToken = managerAppContext.accessToken ?? session.token;
|
||||||
managerAppContext.email = managerAppContext.email ?? session.email;
|
managerAppContext.email = managerAppContext.email ?? session.email;
|
||||||
|
managerAppContext.role = managerAppContext.role ?? UserRole.fromJson(session.role);
|
||||||
|
|
||||||
if(session.instanceId != null) {
|
if(session.instanceId != null) {
|
||||||
if(managerAppContext.clientAPI == null) {
|
if(managerAppContext.clientAPI == null) {
|
||||||
|
|||||||
@ -23,17 +23,17 @@ class UserRole {
|
|||||||
|
|
||||||
int toJson() => value;
|
int toJson() => value;
|
||||||
|
|
||||||
static const number0 = UserRole._(0);
|
static const SuperAdmin = UserRole._(0);
|
||||||
static const number1 = UserRole._(1);
|
static const InstanceAdmin = UserRole._(1);
|
||||||
static const number2 = UserRole._(2);
|
static const ContentEditor = UserRole._(2);
|
||||||
static const number3 = UserRole._(3);
|
static const Viewer = UserRole._(3);
|
||||||
|
|
||||||
/// List of all possible values in this [enum][UserRole].
|
/// List of all possible values in this [enum][UserRole].
|
||||||
static const values = <UserRole>[
|
static const values = <UserRole>[
|
||||||
number0,
|
SuperAdmin,
|
||||||
number1,
|
InstanceAdmin,
|
||||||
number2,
|
ContentEditor,
|
||||||
number3,
|
Viewer,
|
||||||
];
|
];
|
||||||
|
|
||||||
static UserRole? fromJson(dynamic value) =>
|
static UserRole? fromJson(dynamic value) =>
|
||||||
@ -76,15 +76,27 @@ class UserRoleTypeTransformer {
|
|||||||
/// and users are still using an old app with the old code.
|
/// and users are still using an old app with the old code.
|
||||||
UserRole? decode(dynamic data, {bool allowNull = true}) {
|
UserRole? decode(dynamic data, {bool allowNull = true}) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
|
// Handle string values (backend uses JsonStringEnumConverter)
|
||||||
|
if (data is String) {
|
||||||
|
switch (data) {
|
||||||
|
case 'SuperAdmin': return UserRole.SuperAdmin;
|
||||||
|
case 'InstanceAdmin': return UserRole.InstanceAdmin;
|
||||||
|
case 'ContentEditor': return UserRole.ContentEditor;
|
||||||
|
case 'Viewer': return UserRole.Viewer;
|
||||||
|
default:
|
||||||
|
if (!allowNull) throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
switch (data) {
|
switch (data) {
|
||||||
case 0:
|
case 0:
|
||||||
return UserRole.number0;
|
return UserRole.SuperAdmin;
|
||||||
case 1:
|
case 1:
|
||||||
return UserRole.number1;
|
return UserRole.InstanceAdmin;
|
||||||
case 2:
|
case 2:
|
||||||
return UserRole.number2;
|
return UserRole.ContentEditor;
|
||||||
case 3:
|
case 3:
|
||||||
return UserRole.number3;
|
return UserRole.Viewer;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
84
verify_dto.dart
Normal file
84
verify_dto.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
// Mocking the behavior of the DTOs
|
||||||
|
class TranslationDTO {
|
||||||
|
String? language;
|
||||||
|
String? value;
|
||||||
|
TranslationDTO({this.language, this.value});
|
||||||
|
Map<String, dynamic> toJson() => {'language': language, 'value': value};
|
||||||
|
static TranslationDTO fromJson(Map<String, dynamic> json) =>
|
||||||
|
TranslationDTO(language: json['language'], value: json['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventAddressDTOGeometry {
|
||||||
|
String? type;
|
||||||
|
Object? coordinates;
|
||||||
|
EventAddressDTOGeometry({this.type, this.coordinates});
|
||||||
|
Map<String, dynamic> toJson() => {'type': type, 'coordinates': coordinates};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GuidedStepDTO {
|
||||||
|
List<TranslationDTO>? title;
|
||||||
|
EventAddressDTOGeometry? geometry;
|
||||||
|
|
||||||
|
GuidedStepDTO({this.title, this.geometry});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
if (this.title != null) {
|
||||||
|
json['title'] = this.title!.map((v) => v.toJson()).toList();
|
||||||
|
}
|
||||||
|
if (this.geometry != null) {
|
||||||
|
json['geometry'] = this.geometry; // The reported bug: no .toJson() call
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GuidedStepDTO fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
return GuidedStepDTO(
|
||||||
|
title: json['title'] != null
|
||||||
|
? (json['title'] as List)
|
||||||
|
.map((i) => TranslationDTO.fromJson(i))
|
||||||
|
.toList()
|
||||||
|
: null,
|
||||||
|
geometry: json['geometry'] is Map
|
||||||
|
? EventAddressDTOGeometry(
|
||||||
|
type: json['geometry']['type'],
|
||||||
|
coordinates: json['geometry']['coordinates'],
|
||||||
|
)
|
||||||
|
: (json['geometry'] is EventAddressDTOGeometry
|
||||||
|
? json['geometry']
|
||||||
|
: null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GuidedStepDTO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
var geo = EventAddressDTOGeometry(type: 'Point', coordinates: [1.0, 2.0]);
|
||||||
|
var step = GuidedStepDTO(
|
||||||
|
title: [TranslationDTO(language: 'FR', value: 'Test')],
|
||||||
|
geometry: geo,
|
||||||
|
);
|
||||||
|
|
||||||
|
print('Original geometry: ${step.geometry.runtimeType}');
|
||||||
|
|
||||||
|
var jsonMap = step.toJson();
|
||||||
|
print('Value in JSON map for geometry: ${jsonMap['geometry'].runtimeType}');
|
||||||
|
|
||||||
|
// This is what I do in showNewOrUpdateGuidedStep.dart
|
||||||
|
var clonedStep = GuidedStepDTO.fromJson(jsonMap);
|
||||||
|
print('Cloned geometry: ${clonedStep.geometry.runtimeType}');
|
||||||
|
|
||||||
|
// What happens if we actually JSON encode/decode?
|
||||||
|
var encoded = jsonEncode(jsonMap);
|
||||||
|
var decoded = jsonDecode(encoded);
|
||||||
|
print('Decoded value for geometry: ${decoded['geometry'].runtimeType}');
|
||||||
|
|
||||||
|
var clonedStepFromJSON = GuidedStepDTO.fromJson(decoded);
|
||||||
|
print(
|
||||||
|
'Cloned from actual JSON geometry: ${clonedStepFromJSON.geometry.runtimeType}');
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user