import 'dart:developer'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:manager_app/Components/check_input_container.dart'; import 'package:manager_app/Components/confirmation_dialog.dart'; import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_app/Components/common_loader.dart'; import 'package:manager_app/Components/message_notification.dart'; import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_app/Components/multi_select_dropdown_language_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/pane.dart'; import 'package:manager_app/Components/string_input_container.dart'; import 'package:manager_app/Helpers/FileHelper.dart'; import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/Screens/Configurations/section_reorderList.dart'; import 'package:manager_app/app_context.dart'; import 'package:manager_app/client.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_api_new/api.dart'; import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; import 'dart:html' as html; class ConfigurationDetailScreen extends StatefulWidget { final String id; ConfigurationDetailScreen({Key? key, required this.id}) : super(key: key); @override _ConfigurationDetailScreenState createState() => _ConfigurationDetailScreenState(); } class _ConfigurationDetailScreenState extends State { ConfigurationDTO? configurationDTO; List? sections; Future? _configFuture; Future?>? _sectionsFuture; // Les champs sont des TextFormField non contrôlés : leur `initialValue` n'est // lu qu'à la création de l'élément. Changer de DTO ne suffit donc pas à // remettre le formulaire à l'état serveur — il faut forcer une nouvelle // sous-arborescence, d'où ce compteur. int _formRevision = 0; /// L'ordre des sections a été touché mais pas encore écrit. bool _orderDirty = false; @override Widget build(BuildContext context) { final appContext = Provider.of(context); final managerCtx = appContext.getContext() as ManagerAppContext; _configFuture ??= getConfiguration(widget, managerCtx.clientAPI!); return FutureBuilder( future: _configFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data == null) return Center(child: Text(AppLocalizations.of(context)!.noData)); return _buildBody(snapshot.data!, appContext); } else if (snapshot.connectionState == ConnectionState.none) { return Center(child: Text(AppLocalizations.of(context)!.noData)); } return const Center(child: CommonLoader()); }, ); } Widget _buildBody(ConfigurationDTO config, AppContext appContext) { final managerCtx = appContext.getContext() as ManagerAppContext; final canEdit = managerCtx.canEdit; final l = AppLocalizations.of(context)!; return Column( children: [ EditorHeader( title: config.label ?? '', subtitle: l.configHeaderSubtitle( DateFormat('dd/MM/yyyy').format(config.dateCreation!), sections?.length ?? 0, config.languages?.length ?? 0, ), onBack: () { ManagerAppContext ctx = appContext.getContext(); ctx.selectedConfiguration = null; appContext.setContext(ctx); }, actions: [ SquareIconButton( icon: Icons.download_outlined, tooltip: l.configExportAction, onPressed: () => _export(config, appContext), ), if (canEdit) PillButton( label: l.delete, icon: Icons.delete, style: PillButtonStyle.danger, onPressed: () => delete(config, appContext), ), PillButton( label: l.cancel, onPressed: () => cancel(config, appContext), ), if (canEdit) PillButton( label: l.save, icon: Icons.check, style: PillButtonStyle.solid, onPressed: () => save(config, appContext), ), ], ), Expanded( child: SingleChildScrollView( padding: kPagePadding, child: KeyedSubtree( key: ValueKey(_formRevision), child: EditorColumns( main: [ _cardGeneral(config, l), _cardSections(config, appContext), ], rail: [ _cardImages(config, l), ], ), ), ), ), ], ); } // ── Card Général ── Widget _cardGeneral(ConfigurationDTO config, AppLocalizations l) { return Pane( icon: Icons.tune, title: l.settingsPaneTitle, child: LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth > 620; final identifier = StringInputContainer( label: l.identifierLabel, isMandatory: true, initialValue: config.label, onChanged: (value) => config.label = value, ); final title = MultiStringInputContainer( label: l.displayTitleLabel, modalLabel: l.messageTitle, initialValue: config.title ?? [], isTitle: true, isHTML: true, isMandatory: false, maxLines: 1, onGetResult: (value) => setState(() => config.title = value), showPreview: true, ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (isWide) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(flex: 5, child: identifier), const SizedBox(width: kSpace5), Expanded(flex: 7, child: title), ], ) else ...[ identifier, const SizedBox(height: kSpace5), title, ], const SizedBox(height: kSpace5), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( flex: 5, child: MultiSelectDropdownLanguageContainer( label: l.languagesLabel, initialValue: config.languages ?? [], values: languages, isMultiple: true, fontSize: 16, isAtLeastOne: true, onChanged: (value) { config.languages = List.from(value); }, ), ), const SizedBox(width: kSpace5), Expanded( flex: 7, child: Padding( // Alignée sur le champ voisin, pas sur son libellé. padding: const EdgeInsets.only(bottom: kSpace3), child: CheckInputContainer( icon: Icons.signal_wifi_off, label: l.offlineLabel, subtitle: l.offlineHint, isChecked: config.isOffline, onChanged: (value) => config.isOffline = value, ), ), ), ], ), ], ); }, ), ); } // ── Card Images ── Widget _cardImages(ConfigurationDTO config, AppLocalizations l) { return Pane( icon: Icons.image_outlined, title: l.imagesLabel, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ResourceInputContainer( label: l.mainImageLabel, initialValue: config.imageId, onChanged: (ResourceDTO resource) { if (resource.id == null) { config.imageId = null; config.imageSource = null; } else { config.imageId = resource.id; config.imageSource = resource.url; } }, ), const SizedBox(height: kSpace5), ResourceInputContainer( label: l.loaderLabel, initialValue: config.loaderImageId, onChanged: (ResourceDTO resource) { if (resource.id == null) { config.loaderImageId = null; config.loaderImageUrl = null; } else { config.loaderImageId = resource.id; config.loaderImageUrl = resource.url; } }, ), ], ), ); } // ── Card Sections ── Widget _cardSections(ConfigurationDTO config, AppContext appContext) { final managerCtx = appContext.getContext() as ManagerAppContext; _sectionsFuture ??= getSections(config, managerCtx.clientAPI!); return Pane( icon: Icons.dashboard_outlined, title: AppLocalizations.of(context)!.sectionsLabel, child: FutureBuilder?>( future: _sectionsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data == null) return Text(AppLocalizations.of(context)!.noData); final loaded = List.from(snapshot.data!) .where((s) => !s.isSubSection!) .toList(); // Le compte de sections est affiché dans l'en-tête, construit avant // que ce future ne soit résolu : il faut un tour de plus. if (sections?.length != loaded.length) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() {}); }); } sections = loaded; return SectionReorderList( sectionsIn: sections!, configurationId: config.id!, // Le nouvel ordre partait au serveur au relâchement de la souris, // donc « Annuler » n'avait plus rien à annuler. Il attend // maintenant « Enregistrer », comme les autres champs de l'écran. onChangedOrder: (List sectionsOut) { sections = sectionsOut; _orderDirty = true; }, askReload: () => setState(() { _sectionsFuture = null; }), ); } else if (snapshot.connectionState == ConnectionState.none) { return Text(AppLocalizations.of(context)!.noData); } return const Center(child: Padding( padding: EdgeInsets.all(32), child: CommonLoader(), )); }, ), ); } // ── Actions ── void _export(ConfigurationDTO config, AppContext appContext) async { final l = AppLocalizations.of(context)!; try { Client clientAPI = (appContext.getContext() as ManagerAppContext).clientAPI!; await clientAPI.configurationApi!.configurationExport(config.id!); if (kIsWeb) { html.AnchorElement anchorElement = html.AnchorElement(); var uri = Uri.parse('${clientAPI.resourceApi!.apiClient.basePath}/api/Configuration/${config.id}/export'); anchorElement.href = uri.toString(); anchorElement.download = '${config.label}.json'; anchorElement.click(); } else { ExportConfigurationDTO export = await clientAPI.configurationApi!.configurationExport(config.id!); File test = await FileHelper().storeConfiguration(export); showNotification(kSuccess, kWhite, l.configExportSuccess(test.path), context, 3000); } } catch (e) { log(e.toString()); showNotification(kPrimaryColor, kWhite, l.configExportFailed, context, null); } } Future cancel(ConfigurationDTO config, AppContext appContext) async { ManagerAppContext managerAppContext = appContext.getContext(); ConfigurationDTO? configuration = await managerAppContext.clientAPI!.configurationApi!.configurationGetDetail(config.id!); managerAppContext.selectedConfiguration = configuration; appContext.setContext(managerAppContext); if (!mounted) return; setState(() { _configFuture = Future.value(configuration); _sectionsFuture = null; sections = null; _orderDirty = false; _formRevision++; }); } Future delete(ConfigurationDTO config, AppContext appContext) async { showConfirmationDialog( AppLocalizations.of(context)!.configDeleteConfirm, () {}, () async { ManagerAppContext managerAppContext = appContext.getContext(); await managerAppContext.clientAPI!.configurationApi!.configurationDelete(config.id!); managerAppContext.selectedConfiguration = null; appContext.setContext(managerAppContext); showNotification(kSuccess, kWhite, AppLocalizations.of(context)!.configDeletedSuccess, context, null); }, context, ); } Future save(ConfigurationDTO config, AppContext appContext) async { ManagerAppContext managerAppContext = appContext.getContext(); if (_orderDirty && sections != null) { await managerAppContext.clientAPI!.sectionApi!.sectionUpdateOrder(sections!); _orderDirty = false; } ConfigurationDTO? configuration = await managerAppContext.clientAPI!.configurationApi!.configurationUpdate(config); managerAppContext.selectedConfiguration = configuration; appContext.setContext(managerAppContext); showNotification(kSuccess, kWhite, AppLocalizations.of(context)!.configSavedSuccess, context, null); } Future getConfiguration(ConfigurationDetailScreen widget, Client client) async { return await client.configurationApi!.configurationGetDetail(widget.id); } Future?> getSections(ConfigurationDTO config, Client client) async { List? sections = await client.sectionApi!.sectionGetFromConfiguration(config.id!); if (sections != null) { sections.sort((a, b) => a.order!.compareTo(b.order!)); } return sections; } }