From 0c3bc31b5e5f1b4891a6a2d66cdf68a4e67cc913 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Thu, 10 Sep 2026 11:42:15 +0200 Subject: [PATCH] =?UTF-8?q?R=C3=A9glages=20nom=20d'app=20/=20scan=20QR=20/?= =?UTF-8?q?=20liens=20stores,=20et=20nouvelle=20URL=20de=20QR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Écran d'une application : nom de l'application (FR/EN/NL), case d'affichage du scan QR, et liens stores réservés au SuperAdmin. MultiStringInputContainer accepte des langues explicites, faute de configuration sélectionnée sur cet écran. Le QR d'une section pointe vers app.myinfomate.be : la page /download si l'instance a une app mobile, la section web directement sinon. web.mymuseum.be était désuet. Co-Authored-By: Claude Opus 5 (1M context) --- .../multi_string_input_container.dart | 8 +- .../app_configuration_link_screen.dart | 91 +++++++++++++++++++ .../Section/section_detail_screen.dart | 7 +- lib/constants.dart | 4 + lib/l10n/app_en.arb | 6 ++ lib/l10n/app_fr.arb | 6 ++ lib/l10n/app_localizations.dart | 36 ++++++++ lib/l10n/app_localizations_en.dart | 19 ++++ lib/l10n/app_localizations_fr.dart | 19 ++++ lib/l10n/app_localizations_nl.dart | 19 ++++ lib/l10n/app_nl.arb | 6 ++ .../lib/model/application_instance_dto.dart | 48 +++++++++- 12 files changed, 263 insertions(+), 6 deletions(-) diff --git a/lib/Components/multi_string_input_container.dart b/lib/Components/multi_string_input_container.dart index f7fa80f..85ddf19 100644 --- a/lib/Components/multi_string_input_container.dart +++ b/lib/Components/multi_string_input_container.dart @@ -22,6 +22,9 @@ class MultiStringInputContainer extends StatelessWidget { final double fontSize; final bool isMandatory; final bool showPreview; + /// Hors d'une configuration (ex. réglages d'une application), les langues ne + /// peuvent pas venir de `selectedConfiguration` : on les passe explicitement. + final List? languages; const MultiStringInputContainer({ Key? key, @@ -37,6 +40,7 @@ class MultiStringInputContainer extends StatelessWidget { this.fontSize = 18, this.isMandatory = true, this.showPreview = false, + this.languages, }) : super(key: key); String _plainTextPreview(String htmlOrText) { @@ -55,7 +59,7 @@ class MultiStringInputContainer extends StatelessWidget { final appContext = Provider.of(context); final managerAppContext = appContext.getContext(); - final languages = managerAppContext.selectedConfiguration!.languages!; + final languages = this.languages ?? managerAppContext.selectedConfiguration!.languages!; final completed = initialValue.where((t) => (t.value ?? "").trim().isNotEmpty).length; final l = AppLocalizations.of(context)!; @@ -85,7 +89,7 @@ class MultiStringInputContainer extends StatelessWidget { List initials = initialValue; // Préparer les valeurs pour toutes les langues - managerAppContext.selectedConfiguration!.languages!.forEach((lang) { + languages.forEach((lang) { if (initials.any((iv) => iv.language == lang)) { newValues.add(TranslationDTO.fromJson( jsonDecode(jsonEncode(initials.firstWhere((e) => e.language == lang)))!)!); diff --git a/lib/Screens/Applications/app_configuration_link_screen.dart b/lib/Screens/Applications/app_configuration_link_screen.dart index 849ef5f..49bd565 100644 --- a/lib/Screens/Applications/app_configuration_link_screen.dart +++ b/lib/Screens/Applications/app_configuration_link_screen.dart @@ -9,6 +9,9 @@ import 'package:manager_app/Components/confirmation_dialog.dart'; import 'package:manager_app/Components/message_notification.dart'; import 'package:manager_app/Components/multi_select_dropdown_language_container.dart'; import 'package:manager_app/Components/card_shape_selector.dart'; +import 'package:manager_app/Components/check_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/reorderable_custom_list.dart'; import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_app/Components/single_choice_input_container.dart'; @@ -36,6 +39,7 @@ class AppConfigurationLinkScreen extends StatefulWidget { class _AppConfigurationLinkScreenState extends State { late ApplicationInstanceDTO _applicationInstanceDTO; final Map _spanDebounceTimers = {}; + Timer? _storeUrlDebounceTimer; // Largeur de viewport simulée pour l'aperçu Web (le site est responsive, // pas figé — l'admin doit pouvoir vérifier les deux). bool _webPreviewMobileWidth = false; @@ -51,6 +55,7 @@ class _AppConfigurationLinkScreenState extends State for (final timer in _spanDebounceTimers.values) { timer.cancel(); } + _storeUrlDebounceTimer?.cancel(); super.dispose(); } @@ -126,6 +131,22 @@ class _AppConfigurationLinkScreenState extends State ManagerAppContext managerAppContext = appContext.getContext() as ManagerAppContext; final appUpdatedMsg = widget.appUpdatedLabel ?? AppLocalizations.of(context)!.appUpdatedSuccess; + final isVisitorApp = _applicationInstanceDTO.appType == AppType.Mobile || _applicationInstanceDTO.appType == AppType.Web; + final canEditStoreUrls = _applicationInstanceDTO.appType == AppType.Mobile && managerAppContext.role == UserRole.SuperAdmin; + + Future saveApplicationInstance() async { + var applicationInstance = await updateApplicationInstance(appContext, _applicationInstanceDTO); + if (applicationInstance != null && context.mounted) { + showNotification(kSuccess, kWhite, appUpdatedMsg, context, null); + } + } + + // Un champ texte notifie à chaque frappe : on n'enregistre qu'une fois la saisie posée. + void saveStoreUrlDebounced() { + _storeUrlDebounceTimer?.cancel(); + _storeUrlDebounceTimer = Timer(const Duration(milliseconds: 800), saveApplicationInstance); + } + _generalInfoCard() { const elementHeight = 116.0; @@ -334,6 +355,76 @@ class _AppConfigurationLinkScreenState extends State ), ), ), + if (isVisitorApp) + SizedBox( + width: elementWidth, + height: elementHeight, + child: Center( + child: MultiStringInputContainer( + label: AppLocalizations.of(context)!.appNameLabel, + modalLabel: AppLocalizations.of(context)!.appNameModalLabel, + initialValue: _applicationInstanceDTO.appName ?? [], + languages: _applicationInstanceDTO.languages ?? [], + maxLines: 1, + isTitle: true, + isMandatory: false, + onGetResult: (List value) async { + setState(() { + _applicationInstanceDTO.appName = value; + }); + await saveApplicationInstance(); + }, + ), + ), + ), + if (isVisitorApp) + SizedBox( + width: elementWidth, + height: elementHeight, + child: Center( + child: CheckInputContainer( + label: AppLocalizations.of(context)!.qrScanEnabledLabel, + subtitle: AppLocalizations.of(context)!.qrScanEnabledHint, + isChecked: _applicationInstanceDTO.isQRCodeEnabled ?? true, + onChanged: (value) async { + _applicationInstanceDTO.isQRCodeEnabled = value; + await saveApplicationInstance(); + }, + ), + ), + ), + if (canEditStoreUrls) + SizedBox( + width: elementWidth, + height: elementHeight, + child: Center( + child: StringInputContainer( + label: AppLocalizations.of(context)!.appStoreUrlLabel, + initialValue: _applicationInstanceDTO.appStoreUrl ?? "", + maxLength: 300, + onChanged: (value) { + _applicationInstanceDTO.appStoreUrl = value.trim().isEmpty ? null : value.trim(); + saveStoreUrlDebounced(); + }, + ), + ), + ), + if (canEditStoreUrls) + SizedBox( + width: elementWidth, + height: elementHeight, + child: Center( + child: StringInputContainer( + label: AppLocalizations.of(context)!.playStoreUrlLabel, + initialValue: _applicationInstanceDTO.playStoreUrl ?? "", + maxLength: 300, + onChanged: (value) { + _applicationInstanceDTO.playStoreUrl = value.trim().isEmpty ? null : value.trim(); + saveStoreUrlDebounced(); + }, + ), + ), + ), // L'activation de l'assistant par canal se règle dans l'onglet Guide IA, // qui liste tous les canaux au même endroit. ], diff --git a/lib/Screens/Configurations/Section/section_detail_screen.dart b/lib/Screens/Configurations/Section/section_detail_screen.dart index 753618d..ed68bcf 100644 --- a/lib/Screens/Configurations/Section/section_detail_screen.dart +++ b/lib/Screens/Configurations/Section/section_detail_screen.dart @@ -273,7 +273,12 @@ class _SectionDetailScreenState extends State { Widget _cardQR(AppContext appContext, AppLocalizations l) { final managerCtx = appContext.getContext() as ManagerAppContext; - final qrData = "${managerCtx.host!.replaceFirst("api", "web")}/${managerCtx.instanceId}/${managerCtx.selectedConfiguration!.id}/${sectionDTO.id!}"; + final instanceDTO = managerCtx.instanceDTO; + // Sans app mobile (offre web seule), la page de téléchargement n'aurait rien à proposer : + // le QR ouvre directement la section dans l'app web. + final qrData = instanceDTO?.isMobile != true && instanceDTO?.isWeb == true && instanceDTO?.webSlug != null + ? "$kVisitorWebUrl/${instanceDTO!.webSlug}/${managerCtx.selectedConfiguration!.id}/sections/${sectionDTO.id!}" + : "$kVisitorWebUrl/download/${managerCtx.instanceId}/${managerCtx.selectedConfiguration!.id}/${sectionDTO.id!}"; return Pane( icon: Icons.share_outlined, diff --git a/lib/constants.dart b/lib/constants.dart index dc4bc38..0afd2e4 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -23,6 +23,10 @@ const kSuccess = Color(0xFF8bc34a); const kApiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: 'http://localhost:5000'); +// App visiteur web (visitapp-web). Les QR des sections pointent vers sa page +// /download, qui présente l'app mobile de l'instance. +const kVisitorWebUrl = 'https://app.myinfomate.be'; + // Empreinte du build, injectee elle aussi au build : // --dart-define=GIT_SHA=$(git rev-parse --short HEAD) // Elle est affichee sous le titre de l'ecran de login et sert a relier un diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ef8e2a9..8bb3d1e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -637,6 +637,12 @@ "languagesLabel": "Languages:", "featuredEventLabel": "Featured event:", "chooseEvent": "Choose an event", + "appNameLabel": "Application name:", + "appNameModalLabel": "Name shown in the application header", + "qrScanEnabledLabel": "Show the QR code scanner", + "qrScanEnabledHint": "The scan button appears on the home screen and in visits", + "appStoreUrlLabel": "App Store link:", + "playStoreUrlLabel": "Play Store link:", "previewWebTitle": "Preview — Web", "previewMobileTitle": "Preview — Mobile", "previewDesktop": "Desktop", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 334ae4d..401d4e7 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -637,6 +637,12 @@ "languagesLabel": "Langues :", "featuredEventLabel": "Evènement à l'affiche :", "chooseEvent": "Choisir un évènement", + "appNameLabel": "Nom de l'application :", + "appNameModalLabel": "Nom affiché en en-tête de l'application", + "qrScanEnabledLabel": "Afficher le scan de QR code", + "qrScanEnabledHint": "Le bouton de scan apparaît sur l'accueil et dans les visites", + "appStoreUrlLabel": "Lien App Store :", + "playStoreUrlLabel": "Lien Play Store :", "previewWebTitle": "Aperçu — Web", "previewMobileTitle": "Aperçu — Mobile", "previewDesktop": "Desktop", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 20073e2..3484e8f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2860,6 +2860,42 @@ abstract class AppLocalizations { /// **'Choisir un évènement'** String get chooseEvent; + /// No description provided for @appNameLabel. + /// + /// In fr, this message translates to: + /// **'Nom de l\'application :'** + String get appNameLabel; + + /// No description provided for @appNameModalLabel. + /// + /// In fr, this message translates to: + /// **'Nom affiché en en-tête de l\'application'** + String get appNameModalLabel; + + /// No description provided for @qrScanEnabledLabel. + /// + /// In fr, this message translates to: + /// **'Afficher le scan de QR code'** + String get qrScanEnabledLabel; + + /// No description provided for @qrScanEnabledHint. + /// + /// In fr, this message translates to: + /// **'Le bouton de scan apparaît sur l\'accueil et dans les visites'** + String get qrScanEnabledHint; + + /// No description provided for @appStoreUrlLabel. + /// + /// In fr, this message translates to: + /// **'Lien App Store :'** + String get appStoreUrlLabel; + + /// No description provided for @playStoreUrlLabel. + /// + /// In fr, this message translates to: + /// **'Lien Play Store :'** + String get playStoreUrlLabel; + /// No description provided for @previewWebTitle. /// /// In fr, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index d592a9c..5adaa3d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1520,6 +1520,25 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chooseEvent => 'Choose an event'; + @override + String get appNameLabel => 'Application name:'; + + @override + String get appNameModalLabel => 'Name shown in the application header'; + + @override + String get qrScanEnabledLabel => 'Show the QR code scanner'; + + @override + String get qrScanEnabledHint => + 'The scan button appears on the home screen and in visits'; + + @override + String get appStoreUrlLabel => 'App Store link:'; + + @override + String get playStoreUrlLabel => 'Play Store link:'; + @override String get previewWebTitle => 'Preview — Web'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6ca8ab8..1e82af6 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1557,6 +1557,25 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chooseEvent => 'Choisir un évènement'; + @override + String get appNameLabel => 'Nom de l\'application :'; + + @override + String get appNameModalLabel => 'Nom affiché en en-tête de l\'application'; + + @override + String get qrScanEnabledLabel => 'Afficher le scan de QR code'; + + @override + String get qrScanEnabledHint => + 'Le bouton de scan apparaît sur l\'accueil et dans les visites'; + + @override + String get appStoreUrlLabel => 'Lien App Store :'; + + @override + String get playStoreUrlLabel => 'Lien Play Store :'; + @override String get previewWebTitle => 'Aperçu — Web'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 708b969..7d4bc6e 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1536,6 +1536,25 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chooseEvent => 'Kies een evenement'; + @override + String get appNameLabel => 'Naam van de applicatie:'; + + @override + String get appNameModalLabel => 'Naam in de koptekst van de applicatie'; + + @override + String get qrScanEnabledLabel => 'QR-codescanner tonen'; + + @override + String get qrScanEnabledHint => + 'De scanknop verschijnt op het startscherm en in de bezoeken'; + + @override + String get appStoreUrlLabel => 'App Store-link:'; + + @override + String get playStoreUrlLabel => 'Play Store-link:'; + @override String get previewWebTitle => 'Voorbeeld — Web'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index d382567..6be7e65 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -637,6 +637,12 @@ "languagesLabel": "Talen:", "featuredEventLabel": "Uitgelicht evenement:", "chooseEvent": "Kies een evenement", + "appNameLabel": "Naam van de applicatie:", + "appNameModalLabel": "Naam in de koptekst van de applicatie", + "qrScanEnabledLabel": "QR-codescanner tonen", + "qrScanEnabledHint": "De scanknop verschijnt op het startscherm en in de bezoeken", + "appStoreUrlLabel": "App Store-link:", + "playStoreUrlLabel": "Play Store-link:", "previewWebTitle": "Voorbeeld — Web", "previewMobileTitle": "Voorbeeld — Mobiel", "previewDesktop": "Desktop", diff --git a/manager_api_new/lib/model/application_instance_dto.dart b/manager_api_new/lib/model/application_instance_dto.dart index dea62b4..b89b6ea 100644 --- a/manager_api_new/lib/model/application_instance_dto.dart +++ b/manager_api_new/lib/model/application_instance_dto.dart @@ -28,6 +28,10 @@ class ApplicationInstanceDTO { this.sectionEventDTO, this.isAssistant, this.hasStats, + this.isQRCodeEnabled, + this.appName = const [], + this.appStoreUrl, + this.playStoreUrl, }); String? id; @@ -66,6 +70,14 @@ class ApplicationInstanceDTO { bool? hasStats; + bool? isQRCodeEnabled; + + List? appName; + + String? appStoreUrl; + + String? playStoreUrl; + @override bool operator ==(Object other) => identical(this, other) || @@ -84,7 +96,11 @@ class ApplicationInstanceDTO { other.sectionEventId == sectionEventId && other.sectionEventDTO == sectionEventDTO && other.isAssistant == isAssistant && - other.hasStats == hasStats; + other.hasStats == hasStats && + other.isQRCodeEnabled == isQRCodeEnabled && + _deepEquality.equals(other.appName, appName) && + other.appStoreUrl == appStoreUrl && + other.playStoreUrl == playStoreUrl; @override int get hashCode => @@ -103,11 +119,15 @@ class ApplicationInstanceDTO { (sectionEventId == null ? 0 : sectionEventId!.hashCode) + (sectionEventDTO == null ? 0 : sectionEventDTO!.hashCode) + (isAssistant == null ? 0 : isAssistant!.hashCode) + - (hasStats == null ? 0 : hasStats!.hashCode); + (hasStats == null ? 0 : hasStats!.hashCode) + + (isQRCodeEnabled == null ? 0 : isQRCodeEnabled!.hashCode) + + (appName == null ? 0 : appName!.hashCode) + + (appStoreUrl == null ? 0 : appStoreUrl!.hashCode) + + (playStoreUrl == null ? 0 : playStoreUrl!.hashCode); @override String toString() => - 'ApplicationInstanceDTO[id=$id, instanceId=$instanceId, appType=$appType, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, languages=$languages, sectionEventId=$sectionEventId, sectionEventDTO=$sectionEventDTO, isAssistant=$isAssistant, hasStats=$hasStats]'; + 'ApplicationInstanceDTO[id=$id, instanceId=$instanceId, appType=$appType, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, languages=$languages, sectionEventId=$sectionEventId, sectionEventDTO=$sectionEventDTO, isAssistant=$isAssistant, hasStats=$hasStats, isQRCodeEnabled=$isQRCodeEnabled, appName=$appName, appStoreUrl=$appStoreUrl, playStoreUrl=$playStoreUrl]'; Map toJson() { final json = {}; @@ -186,6 +206,24 @@ class ApplicationInstanceDTO { } else { json[r'hasStats'] = null; } + if (this.isQRCodeEnabled != null) { + json[r'isQRCodeEnabled'] = this.isQRCodeEnabled; + } + if (this.appName != null) { + json[r'appName'] = this.appName; + } else { + json[r'appName'] = null; + } + if (this.appStoreUrl != null) { + json[r'appStoreUrl'] = this.appStoreUrl; + } else { + json[r'appStoreUrl'] = null; + } + if (this.playStoreUrl != null) { + json[r'playStoreUrl'] = this.playStoreUrl; + } else { + json[r'playStoreUrl'] = null; + } return json; } @@ -230,6 +268,10 @@ class ApplicationInstanceDTO { sectionEventDTO: SectionEventDTO.fromJson(json[r'sectionEventDTO']), isAssistant: mapValueOfType(json, r'isAssistant'), hasStats: mapValueOfType(json, r'hasStats'), + isQRCodeEnabled: mapValueOfType(json, r'isQRCodeEnabled'), + appName: TranslationDTO.listFromJson(json[r'appName']), + appStoreUrl: mapValueOfType(json, r'appStoreUrl'), + playStoreUrl: mapValueOfType(json, r'playStoreUrl'), ); } return null;