Écran « Activité » sur GET /api/Audit, réservé au SuperAdmin comme la policy de l'endpoint (menuId 13, conditionné à role.value == 0). Filtres instance, type d'entité, utilisateur et plage de dates, pagination 50, clic sur une ligne pour le détail avant/après en table plutôt que le JSON brut. Les ids d'instance et d'utilisateur sont résolus en noms, un id absent de l'annuaire restant affiché tel quel : le journal doit rester lisible après la suppression de ce qu'il décrit. manager_api_new n'est pas touché. L'écran passe par un http.get direct au Bearer, comme _loadKnowledge / _loadInsights / _reindex du Guide IA : une lecture seule ne justifie pas d'étendre un client qui s'édite à la main, et le déclencheur de génération n'existe plus depuis le lot A. Côté utilisateurs, compteur « X / 5 » et bouton d'ajout désactivé au plafond. Le SuperAdmin en est exclu : son GET /api/User renvoie toutes les instances, compter cette liste contre un plafond par instance n'aurait aucun sens. Le masquage du rôle SuperAdmin pour un InstanceAdmin, resté en question dans todo-features.md, était déjà fait — _allowedRoles filtre sur r >= callerRole. Trois pièges relevés en câblant : - AuditController renvoie les entités brutes, pas un DTO — les champs sont ceux d'AuditLog en camelCase. - invokeAPI ne lève pas sur un code d'erreur et son résultat était ignoré dans users_screen : un e-mail déjà utilisé (409) ou un rôle refusé (403) ne produisait aucun message. Le statut est désormais testé et le corps affiché, ce qui rendra visible sans retouche le futur 422 du plafond. - DropdownButtonFormField ne relit pas initialValue sur reconstruction (FormField.didUpdateWidget ne traite que forceErrorText) : « Réinitialiser les filtres » vidait la requête sans vider l'affichage. Remplacé par un DropdownButton piloté. 40 clés i18n FR/EN/NL, flutter gen-l10n relancé. flutter analyze propre sur les dossiers travaillés, flutter build web vert. Deux dettes serveur restent ouvertes, hors périmètre de ce commit : le plafond de 5 n'est pas appliqué par UserController (ce qui est livré ici est un garde-fou d'interface, un POST direct passe toujours), et aucune section n'est journalisée — AuditedTypes.Contains exige l'égalité exacte alors que Section est abstraite. Détail dans DOCS/todo-features.md et v1-plan.md lot F. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
779 lines
34 KiB
Dart
779 lines
34 KiB
Dart
import 'dart:html';
|
|
|
|
import 'package:auto_size_text/auto_size_text.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_svg/svg.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:manager_app/Models/managerContext.dart';
|
|
import 'package:manager_app/Models/menu.dart';
|
|
import 'package:manager_app/Models/menuSection.dart';
|
|
import 'package:manager_app/Screens/ApiKeys/api_keys_screen.dart';
|
|
import 'package:manager_app/Screens/Audit/audit_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/Resources/resources_screen.dart';
|
|
import 'package:manager_app/Screens/Statistics/statistics_screen.dart';
|
|
import 'package:manager_app/Screens/GuideIa/guide_ia_screen.dart';
|
|
import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart';
|
|
import 'package:manager_app/Screens/Applications/web_app_screen.dart';
|
|
import 'package:manager_app/Screens/Billing/subscription_screen.dart';
|
|
import 'package:manager_app/Screens/Notifications/notifications_screen.dart';
|
|
import 'package:manager_app/Screens/Users/users_screen.dart';
|
|
import 'package:manager_app/l10n/app_localizations.dart';
|
|
import 'package:manager_app/app_context.dart';
|
|
import 'package:manager_app/Components/quota_bars_widget.dart';
|
|
import 'package:manager_app/constants.dart';
|
|
import 'package:manager_app/main.dart';
|
|
import 'package:manager_api_new/api.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
class MainScreen extends StatefulWidget {
|
|
final InstanceDTO instance;
|
|
final String? view;
|
|
MainScreen({Key? key, required this.instance, required this.view}) : super(key: key);
|
|
|
|
@override
|
|
_MainScreenState createState() => _MainScreenState();
|
|
}
|
|
|
|
class _MainScreenState extends State<MainScreen> {
|
|
late MenuSection devices;
|
|
late MenuSection configurations;
|
|
late MenuSection resources;
|
|
|
|
Menu menu = Menu(title: "MyInfoMate");
|
|
Widget? selectedElement = 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
|
|
void initState() {
|
|
super.initState();
|
|
devices = MenuSection(name: "Applications", type: "devices", menuId: 0, subMenu: []);
|
|
if (widget.instance.isMobile!) devices.subMenu.add(MenuSection(name: "Mobile", type: "mobile", menuId: 1, subMenu: []));
|
|
if (widget.instance.isTablet!) devices.subMenu.add(MenuSection(name: "Kiosk", type: "kiosk", menuId: 2, subMenu: []));
|
|
if (widget.instance.isWeb!) devices.subMenu.add(MenuSection(name: "Web", type: "web", menuId: 3, subMenu: []));
|
|
if (widget.instance.isVR!) devices.subMenu.add(MenuSection(name: "VR", type: "vr", menuId: 4, subMenu: []));
|
|
|
|
configurations = MenuSection(name: "Configurations", type: "configurations", menuId: 5, subMenu: []);
|
|
resources = MenuSection(name: "Ressources", type: "resources", menuId: 6, subMenu: []);
|
|
|
|
menu.sections = [devices, configurations, resources];
|
|
if (widget.instance.hasStats == true) {
|
|
menu.sections!.add(MenuSection(name: "Statistiques", type: "statistics", menuId: 7, subMenu: []));
|
|
}
|
|
if (widget.instance.isAssistant == true) {
|
|
menu.sections!.add(MenuSection(name: "Guide IA", type: "guide-ia", menuId: 12, subMenu: []));
|
|
}
|
|
|
|
|
|
if(currentPosition.value == null) {
|
|
if (widget.instance.isMobile!) {
|
|
currentPosition.value = 1;
|
|
} else if (widget.instance.isTablet!) {
|
|
currentPosition.value = 2;
|
|
} else if (widget.instance.isWeb!) {
|
|
currentPosition.value = 3;
|
|
} else {
|
|
currentPosition.value = 4;
|
|
}
|
|
} else {
|
|
//currentPosition = managerAppContext.currentPositionMenu!;
|
|
}
|
|
|
|
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
|
|
}
|
|
|
|
Future<void> _showAssignPlanDialog(BuildContext context, ManagerAppContext managerCtx, Instance inst, {void Function()? onSaved}) async {
|
|
List<SubscriptionPlanDTO>? plans;
|
|
InstanceDTO? instanceDetail;
|
|
try {
|
|
plans = await managerCtx.clientAPI!.subscriptionPlanApi!.subscriptionPlanGet();
|
|
instanceDetail = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id);
|
|
} catch (_) {}
|
|
|
|
if (!context.mounted) return;
|
|
|
|
final planList = plans ?? [];
|
|
String? selectedPlanId = instanceDetail?.subscriptionPlanId;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) => StatefulBuilder(
|
|
builder: (dialogContext, setDialogState) => AlertDialog(
|
|
title: Text('Plan — ${inst.name}'),
|
|
content: SizedBox(
|
|
width: 360,
|
|
child: planList.isEmpty
|
|
? Text(AppLocalizations.of(context)!.noPlansAvailable)
|
|
: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
RadioListTile<String?>(
|
|
title: Text(AppLocalizations.of(context)!.noPlan),
|
|
value: null,
|
|
groupValue: selectedPlanId,
|
|
onChanged: (v) => setDialogState(() => selectedPlanId = v),
|
|
),
|
|
...planList.map((plan) => RadioListTile<String?>(
|
|
title: Text(plan.name),
|
|
subtitle: Text(
|
|
'${_formatQuotaBytes(plan.storageQuotaBytes, unlimitedLabel: AppLocalizations.of(dialogContext)!.unlimitedStorage)} · ${plan.aiTokensPerMonth == 0 ? AppLocalizations.of(dialogContext)!.unlimitedAI : AppLocalizations.of(dialogContext)!.aiTokensPerMonth(_formatTokens(plan.aiTokensPerMonth))}',
|
|
style: const TextStyle(fontSize: 11),
|
|
),
|
|
value: plan.id,
|
|
groupValue: selectedPlanId,
|
|
onChanged: (v) => setDialogState(() => selectedPlanId = v),
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(dialogContext, rootNavigator: true).pop(),
|
|
child: Text(AppLocalizations.of(dialogContext)!.cancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
Navigator.of(dialogContext, rootNavigator: true).pop();
|
|
try {
|
|
await managerCtx.clientAPI!.instanceApi!.instanceUpdateinstance(
|
|
InstanceDTO(
|
|
id: instanceDetail?.id,
|
|
name: instanceDetail?.name,
|
|
pinCode: instanceDetail?.pinCode,
|
|
isPushNotification: instanceDetail?.isPushNotification,
|
|
hasStats: instanceDetail?.hasStats,
|
|
hasAdvancedStats: instanceDetail?.hasAdvancedStats,
|
|
isMobile: instanceDetail?.isMobile,
|
|
isTablet: instanceDetail?.isTablet,
|
|
isWeb: instanceDetail?.isWeb,
|
|
isVR: instanceDetail?.isVR,
|
|
isAssistant: instanceDetail?.isAssistant,
|
|
aiTokensThisMonth: instanceDetail?.aiTokensThisMonth,
|
|
aiUsageMonthKey: instanceDetail?.aiUsageMonthKey,
|
|
subscriptionPlanId: selectedPlanId ?? '',
|
|
),
|
|
);
|
|
onSaved?.call();
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.planUpdated)));
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errorMessage(e.toString()))));
|
|
}
|
|
}
|
|
},
|
|
child: Text(AppLocalizations.of(dialogContext)!.save),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static String _formatQuotaBytes(int? bytes, {String? unlimitedLabel}) {
|
|
if (bytes == null || bytes == 0) return unlimitedLabel ?? 'Stockage illimité';
|
|
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(0)} MB';
|
|
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
|
}
|
|
|
|
static String _formatTokens(int? tokens) {
|
|
if (tokens == null || tokens == 0) return '0';
|
|
if (tokens < 1000) return '$tokens';
|
|
if (tokens < 1000000) return '${(tokens / 1000).toStringAsFixed(0)}K';
|
|
return '${(tokens / 1000000).toStringAsFixed(1)}M';
|
|
}
|
|
|
|
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: Text(AppLocalizations.of(context)!.switchInstance),
|
|
content: SizedBox(
|
|
width: 400,
|
|
child: instanceList.isEmpty
|
|
? Text(AppLocalizations.of(context)!.noInstanceFound)
|
|
: 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: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (isCurrent) const Icon(Icons.check, color: Colors.green),
|
|
IconButton(
|
|
icon: const Icon(Icons.edit_outlined, size: 18),
|
|
tooltip: AppLocalizations.of(context)!.configurePlan,
|
|
onPressed: () => _showAssignPlanDialog(context, managerCtx, inst, onSaved: () {
|
|
Navigator.of(context, rootNavigator: true).pop();
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
onTap: isCurrent
|
|
? null
|
|
: () async {
|
|
Navigator.of(context, rootNavigator: true).pop();
|
|
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.of(context, rootNavigator: true).pop(), child: Text(AppLocalizations.of(context)!.close)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
static String _localizedSectionName(BuildContext context, String type) {
|
|
final l = AppLocalizations.of(context)!;
|
|
switch (type) {
|
|
case 'devices': return l.menuApplications;
|
|
case 'configurations': return l.menuConfigurations;
|
|
case 'resources': return l.menuResources;
|
|
case 'statistics': return l.menuStatistics;
|
|
case 'guide-ia': return l.menuGuideIa;
|
|
case 'notifications': return l.menuNotifications;
|
|
case 'users': return l.menuUsers;
|
|
case 'apikeys': return l.menuApiKeys;
|
|
case 'audit': return l.menuAudit;
|
|
case 'subscription': return l.menuSubscription;
|
|
default: return type;
|
|
}
|
|
}
|
|
|
|
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 'guide-ia': return Icons.record_voice_over_outlined;
|
|
case 'notifications': return Icons.notifications_none;
|
|
case 'users': return Icons.people_outline;
|
|
case 'apikeys': return Icons.vpn_key_outlined;
|
|
case 'audit': return Icons.history;
|
|
case 'subscription': return Icons.workspace_premium_outlined;
|
|
default: return Icons.circle_outlined;
|
|
}
|
|
}
|
|
|
|
Widget buildMenu(BuildContext context, AppContext appContext, ManagerAppContext managerAppContext, bool isDrawer) {
|
|
return Container(
|
|
width: isDrawer ? null : 250, // fixed width on sidebar, null on drawer for full width
|
|
child: Card(
|
|
color: kWhite,
|
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
|
elevation: 0,
|
|
child: Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Container(
|
|
height: 150,
|
|
width: 250,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
constraints: BoxConstraints(maxHeight: 60, minHeight: 40),
|
|
child: SvgPicture.asset('assets/images/MyInfoMate_logo_only.svg')
|
|
),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
menu.title,
|
|
style: TextStyle(
|
|
color: kPrimaryColor,
|
|
fontSize: 25,
|
|
fontWeight: FontWeight.w400,
|
|
fontFamily: "Helvetica",
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 25),
|
|
Expanded(
|
|
child: Theme(
|
|
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
|
child: ListView(
|
|
padding: EdgeInsets.zero,
|
|
children: menu.sections!.map((section) {
|
|
final router = GoRouter.of(context);
|
|
final routeMatchList = router.routerDelegate.currentConfiguration;
|
|
var currentPath = routeMatchList.isNotEmpty ? routeMatchList.matches.first.matchedLocation : null;
|
|
|
|
if (section.subMenu.isEmpty) {
|
|
return Container(
|
|
key: ValueKey(section.type),
|
|
decoration: currentPath!.contains(section.type)
|
|
? BoxDecoration(
|
|
border: Border(
|
|
right: BorderSide(
|
|
color: kPrimaryColor,
|
|
width: 2,
|
|
),
|
|
),
|
|
)
|
|
: null,
|
|
child: ListTile(
|
|
leading: _NudgeIcon(
|
|
key: _nudgeKey(section.type),
|
|
icon: _sectionIcon(section.type),
|
|
isActive: currentPath.contains(section.type),
|
|
color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor,
|
|
size: 22,
|
|
),
|
|
title: Text(_localizedSectionName(context, section.type), style: TextStyle(color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: currentPath.contains(section.type) ? FontWeight.w500 : FontWeight.w100)),
|
|
selected: currentPosition == section.menuId,
|
|
onTap: () {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
|
_nudgeKey(section.type).currentState?.nudge());
|
|
context.go('/main/${section.type}');
|
|
if (isDrawer) Navigator.of(context).pop();
|
|
},
|
|
),
|
|
);
|
|
} else {
|
|
final isAppActive = currentPath!.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr");
|
|
return Container(
|
|
key: ValueKey(section.type),
|
|
child: ExpansionTile(
|
|
leading: _NudgeIcon(
|
|
key: _nudgeKey(section.type),
|
|
icon: _sectionIcon(section.type),
|
|
isActive: isAppActive,
|
|
color: isAppActive ? kPrimaryColor : kBodyTextColor,
|
|
size: 22,
|
|
),
|
|
iconColor: isAppActive ? kPrimaryColor : kBodyTextColor,
|
|
collapsedIconColor: isAppActive ? kPrimaryColor : kBodyTextColor,
|
|
title: Text(_localizedSectionName(context, section.type), style: TextStyle(color: isAppActive ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: isAppActive ? FontWeight.w500 : FontWeight.w100)),
|
|
children: section.subMenu.map((subSection) {
|
|
return Container(
|
|
decoration: currentPath.contains(subSection.type)
|
|
? BoxDecoration(
|
|
border: Border(
|
|
right: BorderSide(
|
|
color: kPrimaryColor,
|
|
width: 2,
|
|
),
|
|
),
|
|
) : null,
|
|
child: ListTile(
|
|
title: Padding(
|
|
padding: const EdgeInsets.only(left: 16.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
subSection.type == "mobile" ? Icon(Icons.phone_iphone, color: currentPath.contains(subSection.type) ? kPrimaryColor : kBodyTextColor, size: 20) :
|
|
subSection.type == "kiosk" ? Icon(Icons.tablet_rounded, color: currentPath.contains(subSection.type) ? kPrimaryColor : kBodyTextColor, size: 20) :
|
|
subSection.type == "web" ? Icon(Icons.public_outlined, color: currentPath.contains(subSection.type) ? kPrimaryColor : kBodyTextColor, size: 20) :
|
|
subSection.type == "vr" ? Icon(Icons.panorama_photosphere, color: currentPath.contains(subSection.type)? kPrimaryColor : kBodyTextColor, size: 20) : SizedBox(),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(_localizedSectionName(context, subSection.type), style: TextStyle(color: currentPath.contains(subSection.type) ? kPrimaryColor : kBodyTextColor, fontSize: 18)),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
selected: currentPosition.value == subSection.menuId,
|
|
onTap: () {
|
|
if (currentPath != null && currentPath.contains(subSection.type)) {
|
|
// already on this page
|
|
} else {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
|
_nudgeKey(section.type).currentState?.nudge());
|
|
context.go('/main/${subSection.type}');
|
|
}
|
|
if (isDrawer) Navigator.of(context).pop();
|
|
},
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
// Footer: Quota + Language + Email + Switch instance (SuperAdmin) + Logout
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Column(
|
|
children: [
|
|
const QuotaBarsWidget(),
|
|
Row(
|
|
children: [
|
|
DropdownButton<Locale>(
|
|
value: managerAppContext.locale,
|
|
underline: const SizedBox(),
|
|
isDense: true,
|
|
items: const [
|
|
DropdownMenuItem(value: Locale('fr'), child: Text('🇫🇷 FR')),
|
|
DropdownMenuItem(value: Locale('en'), child: Text('🇬🇧 EN')),
|
|
DropdownMenuItem(value: Locale('nl'), child: Text('🇧🇪 NL')),
|
|
],
|
|
onChanged: (locale) {
|
|
if (locale != null) appContext.setLocale(locale);
|
|
},
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: AutoSizeText(
|
|
(appContext.getContext() as ManagerAppContext).email ?? "",
|
|
style: TextStyle(color: kBodyTextColor, fontSize: 14, fontWeight: FontWeight.w300, fontFamily: "Helvetica"),
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
if ((appContext.getContext() as ManagerAppContext).role == UserRole.SuperAdmin)
|
|
IconButton(
|
|
icon: Icon(Icons.swap_horiz, color: kPrimaryColor),
|
|
tooltip: AppLocalizations.of(context)!.tooltipSwitchInstance,
|
|
onPressed: () => _showSwitchInstanceDialog(context, appContext.getContext() as ManagerAppContext),
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.logout, color: kPrimaryColor),
|
|
tooltip: AppLocalizations.of(context)!.tooltipLogout,
|
|
onPressed: () async {
|
|
await loadJsonSessionFile();
|
|
setState(() {
|
|
Storage localStorage = window.localStorage;
|
|
localStorage.clear();
|
|
ManagerAppContext managerAppContext = appContext.getContext();
|
|
managerAppContext.accessToken = null;
|
|
managerAppContext.instanceId = null;
|
|
managerAppContext.instanceDTO = null;
|
|
appContext.setContext(managerAppContext);
|
|
|
|
context.go('/login');
|
|
});
|
|
},
|
|
),
|
|
],
|
|
)
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final appContext = Provider.of<AppContext>(context);
|
|
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);
|
|
final hasNotifItem = menu.sections!.any((s) => s.menuId == 10);
|
|
final hasSubscriptionItem = menu.sections!.any((s) => s.menuId == 11);
|
|
if (managerAppContext.instanceDTO?.subscriptionPlanId == "plan-essentiel" && !hasSubscriptionItem) {
|
|
menu.sections!.add(MenuSection(name: "Abonnement", type: "subscription", menuId: 11, subMenu: []));
|
|
} else if (managerAppContext.instanceDTO?.subscriptionPlanId != "plan-essentiel" && hasSubscriptionItem) {
|
|
menu.sections!.removeWhere((s) => s.menuId == 11);
|
|
}
|
|
if (role != null && role.value <= 1 && managerAppContext.instanceDTO?.isPushNotification == true && !hasNotifItem) {
|
|
menu.sections!.add(MenuSection(name: "Notifications", type: "notifications", menuId: 10, subMenu: []));
|
|
} else if ((role == null || role.value > 1 || managerAppContext.instanceDTO?.isPushNotification != true) && hasNotifItem) {
|
|
menu.sections!.removeWhere((s) => s.menuId == 10);
|
|
}
|
|
if (role != null && role.value <= 1 && !hasAdminItems) {
|
|
menu.sections!.add(MenuSection(name: "Utilisateurs", type: "users", menuId: 8, subMenu: []));
|
|
menu.sections!.add(MenuSection(name: "Clés API", type: "apikeys", menuId: 9, subMenu: []));
|
|
} else if ((role == null || role.value > 1) && hasAdminItems) {
|
|
menu.sections!.removeWhere((s) => s.menuId == 8 || s.menuId == 9);
|
|
}
|
|
// Le journal d'activité couvre toutes les instances et expose les valeurs avant/après :
|
|
// l'endpoint est sous policy SuperAdmin, l'entrée de menu suit la même règle.
|
|
final hasAuditItem = menu.sections!.any((s) => s.menuId == 13);
|
|
if (role != null && role.value == 0 && !hasAuditItem) {
|
|
menu.sections!.add(MenuSection(name: "Activité", type: "audit", menuId: 13, subMenu: []));
|
|
} else if ((role == null || role.value != 0) && hasAuditItem) {
|
|
menu.sections!.removeWhere((s) => s.menuId == 13);
|
|
}
|
|
|
|
Size size = MediaQuery.of(context).size;
|
|
bool isMobile = size.width < 850;
|
|
|
|
return Scaffold(
|
|
appBar: isMobile
|
|
? AppBar(
|
|
title: Text(menu.title, style: TextStyle(color: kWhite)),
|
|
backgroundColor: kPrimaryColor,
|
|
leading: Builder(
|
|
builder: (context) => IconButton(
|
|
icon: Icon(Icons.menu, color: kWhite),
|
|
onPressed: () => Scaffold.of(context).openDrawer(),
|
|
),
|
|
),
|
|
)
|
|
: null,
|
|
drawer: isMobile ? ValueListenableBuilder<int?>(
|
|
valueListenable: currentPosition,
|
|
builder: (context, value, _) {
|
|
return Drawer(child: buildMenu(context, appContext, managerAppContext, true));
|
|
}
|
|
) : null,
|
|
body: Row(
|
|
children: [
|
|
if (!isMobile)
|
|
ValueListenableBuilder<int?>(
|
|
valueListenable: currentPosition,
|
|
builder: (context, value, _) {
|
|
return buildMenu(context, appContext, managerAppContext, false);
|
|
}
|
|
),
|
|
Expanded(
|
|
child: ValueListenableBuilder<int?>(
|
|
valueListenable: currentPosition,
|
|
builder: (context, value, _) {
|
|
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: selectedElement,
|
|
);
|
|
}
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
initElementToShow(BuildContext context, String? view, int currentPosition, Menu menu, InstanceDTO instanceDTO) {
|
|
|
|
if(view != null) {
|
|
switch (view) {
|
|
case "mobile":
|
|
currentPosition = 1;
|
|
break;
|
|
case "kiosk":
|
|
currentPosition = 2;
|
|
break;
|
|
case "web":
|
|
currentPosition = 3;
|
|
break;
|
|
case "vr":
|
|
currentPosition = 4;
|
|
break;
|
|
case "configurations":
|
|
currentPosition = 5;
|
|
break;
|
|
case "resources":
|
|
currentPosition = 6;
|
|
break;
|
|
case "statistics":
|
|
currentPosition = 7;
|
|
break;
|
|
case "users":
|
|
currentPosition = 8;
|
|
break;
|
|
case "apikeys":
|
|
currentPosition = 9;
|
|
break;
|
|
case "notifications":
|
|
currentPosition = 10;
|
|
break;
|
|
case "subscription":
|
|
currentPosition = 11;
|
|
break;
|
|
case "guide-ia":
|
|
currentPosition = 12;
|
|
break;
|
|
case "audit":
|
|
currentPosition = 13;
|
|
break;
|
|
}
|
|
}
|
|
|
|
MenuSection? elementToShow = menu.sections!.firstWhereOrNull((s) => s.menuId == currentPosition);
|
|
|
|
if(elementToShow == null) {
|
|
elementToShow = menu.sections![0].subMenu.where((s) => s.menuId == currentPosition).first;
|
|
}
|
|
|
|
switch (elementToShow.type) {
|
|
case 'mobile' :
|
|
var applicationInstanceMobile = instanceDTO.applicationInstanceDTOs?.firstWhereOrNull((ai) => ai.appType == AppType.Mobile);
|
|
if (applicationInstanceMobile == null) return const Center(child: Text('Application mobile non configurée'));
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: AppConfigurationLinkScreen(applicationInstanceDTO: applicationInstanceMobile)
|
|
);
|
|
case 'kiosk' :
|
|
var applicationInstanceTablet = instanceDTO.applicationInstanceDTOs?.firstWhereOrNull((ai) => ai.appType == AppType.Tablet);
|
|
if (applicationInstanceTablet == null) return const Center(child: Text('Application kiosk non configurée'));
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: KioskScreen(applicationInstanceDTO: applicationInstanceTablet)
|
|
);
|
|
case 'web' :
|
|
return const WebInstanceLoader();
|
|
case 'vr' :
|
|
var applicationInstanceVR = instanceDTO.applicationInstanceDTOs?.firstWhereOrNull((ai) => ai.appType == AppType.VR);
|
|
if (applicationInstanceVR == null) return const Center(child: Text('Application VR non configurée'));
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text("TODO vr")
|
|
);
|
|
case 'configurations' :
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: ConfigurationsScreen()
|
|
);
|
|
case 'resources' :
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: ResourcesScreen(
|
|
resourceTypes: [
|
|
ResourceType.Audio,
|
|
ResourceType.Image,
|
|
ResourceType.ImageUrl,
|
|
ResourceType.Video,
|
|
ResourceType.VideoUrl,
|
|
ResourceType.Pdf,
|
|
ResourceType.Json,
|
|
ResourceType.JsonUrl
|
|
]
|
|
)
|
|
);
|
|
case 'statistics' :
|
|
return const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: StatisticsScreen()
|
|
);
|
|
case 'guide-ia' :
|
|
return const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: GuideIaScreen()
|
|
);
|
|
case 'users':
|
|
return const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: UsersScreen()
|
|
);
|
|
case 'apikeys':
|
|
return const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: ApiKeysScreen()
|
|
);
|
|
case 'audit':
|
|
return const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: AuditScreen()
|
|
);
|
|
case 'notifications':
|
|
return const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: NotificationsScreen()
|
|
);
|
|
case 'subscription':
|
|
return const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: SubscriptionScreen()
|
|
);
|
|
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),
|
|
);
|
|
}
|
|
} |