From c85fab8ca31f49ffe0b1a7d1272d45d5838d4762 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Thu, 23 Jul 2026 09:42:01 +0200 Subject: [PATCH 1/2] feat(frontend): download responsable history csv --- .../lib/screens/responsable_screen.dart | 49 +++++++++++++---- eme-frontend/lib/services/api_client.dart | 55 +++++++++++++++++++ .../lib/services/responsable_service.dart | 6 ++ eme-frontend/lib/utils/file_download.dart | 21 +++++++ 4 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 eme-frontend/lib/utils/file_download.dart diff --git a/eme-frontend/lib/screens/responsable_screen.dart b/eme-frontend/lib/screens/responsable_screen.dart index 32536d4..b1490f2 100644 --- a/eme-frontend/lib/screens/responsable_screen.dart +++ b/eme-frontend/lib/screens/responsable_screen.dart @@ -4,6 +4,7 @@ import "package:google_fonts/google_fonts.dart"; import "../demo_identity.dart"; import "../services/responsable_service.dart"; import "../theme/ensup_colors.dart"; +import "../utils/file_download.dart"; const _workspaceBackground = Color(0xFFF3F6F9); const _orange = Color(0xFFD97706); @@ -19,6 +20,7 @@ class _ResponsableScreenState extends State { late Future<_ResponsableData> _future = _charger(); int _onglet = 0; bool _transitionEnCours = false; + bool _exportEnCours = false; Future<_ResponsableData> _charger() async { final dashboard = await ResponsableService.dashboard(); @@ -79,6 +81,33 @@ class _ResponsableScreenState extends State { } } + Future _exporterHistorique() async { + if (_exportEnCours) return; + + setState(() => _exportEnCours = true); + try { + final file = await ResponsableService.exporterHistorique(); + downloadFile( + bytes: file.bytes, + fileName: file.fileName, + contentType: file.contentType, + ); + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text("Export CSV téléchargé."))); + } catch (error) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(error.toString()))); + } finally { + if (mounted) { + setState(() => _exportEnCours = false); + } + } + } + String? _prochainStatut(String? statut) { return switch (statut) { "DETECTEE" => "EN_COURS_TRAITEMENT", @@ -195,17 +224,15 @@ class _ResponsableScreenState extends State { items: data.historique, builder: _historiqueTile, action: OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - "Export disponible via /api/responsable/historique/export.csv", - ), - ), - ); - }, - icon: const Icon(Icons.download_outlined, size: 16), - label: const Text("Export CSV"), + onPressed: _exportEnCours ? null : _exporterHistorique, + icon: _exportEnCours + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.download_outlined, size: 16), + label: Text(_exportEnCours ? "Export en cours" : "Export CSV"), ), searchableText: (item) => "${item["action"]} ${item["description"]}", ), diff --git a/eme-frontend/lib/services/api_client.dart b/eme-frontend/lib/services/api_client.dart index ed32478..c7d7d74 100644 --- a/eme-frontend/lib/services/api_client.dart +++ b/eme-frontend/lib/services/api_client.dart @@ -1,4 +1,5 @@ import "dart:convert"; +import "dart:typed_data"; import "package:http/http.dart" as http; /// Erreur levée par le client API (serveur injoignable ou réponse en échec). @@ -11,6 +12,18 @@ class ApiException implements Exception { String toString() => message; } +class ApiFile { + const ApiFile({ + required this.bytes, + required this.fileName, + required this.contentType, + }); + + final Uint8List bytes; + final String fileName; + final String contentType; +} + /// Point d'accès unique au backend : centralise l'URL de base, l'en-tête /// d'authentification (simulée) et la gestion des erreurs. class ApiClient { @@ -44,6 +57,37 @@ class ApiClient { throw ApiException(_messageErreur(reponse)); } + static Future getFile( + String chemin, { + required String fallbackFileName, + String utilisateurEmail = _utilisateurEmail, + }) async { + final uri = Uri.parse("$_baseUrl$chemin"); + + late final http.Response reponse; + try { + reponse = await http.get( + uri, + headers: {"x-user-email": utilisateurEmail}, + ); + } catch (_) { + throw const ApiException( + "Impossible de joindre le serveur. Vérifiez que le backend est démarré.", + ); + } + + if (reponse.statusCode < 200 || reponse.statusCode >= 300) { + throw ApiException(_messageErreur(reponse)); + } + + return ApiFile( + bytes: reponse.bodyBytes, + fileName: _fileName(reponse, fallbackFileName), + contentType: + reponse.headers["content-type"] ?? "application/octet-stream", + ); + } + static Future post( String chemin, Map body, { @@ -116,4 +160,15 @@ class ApiClient { } return "Erreur ${reponse.statusCode}"; } + + static String _fileName(http.Response reponse, String fallback) { + final disposition = reponse.headers["content-disposition"]; + if (disposition == null) return fallback; + + final match = RegExp( + "filename=\"?([^\";]+)\"?", + caseSensitive: false, + ).firstMatch(disposition); + return match?.group(1) ?? fallback; + } } diff --git a/eme-frontend/lib/services/responsable_service.dart b/eme-frontend/lib/services/responsable_service.dart index 5bec702..62e6b6b 100644 --- a/eme-frontend/lib/services/responsable_service.dart +++ b/eme-frontend/lib/services/responsable_service.dart @@ -29,6 +29,12 @@ class ResponsableService { static Future>> historique() => _liste("/responsable/historique"); + static Future exporterHistorique() => ApiClient.getFile( + "/responsable/historique/export.csv", + fallbackFileName: "historique-responsable.csv", + utilisateurEmail: _email, + ); + static Future marquerNotificationsLues() async { final reponse = await ApiClient.patch( "/responsable/notifications/lu-toutes", diff --git a/eme-frontend/lib/utils/file_download.dart b/eme-frontend/lib/utils/file_download.dart new file mode 100644 index 0000000..810c5a3 --- /dev/null +++ b/eme-frontend/lib/utils/file_download.dart @@ -0,0 +1,21 @@ +// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use + +import "dart:html" as html; +import "dart:typed_data"; + +void downloadFile({ + required Uint8List bytes, + required String fileName, + required String contentType, +}) { + final blob = html.Blob([bytes], contentType); + final url = html.Url.createObjectUrlFromBlob(blob); + final anchor = html.AnchorElement(href: url) + ..download = fileName + ..style.display = "none"; + + html.document.body?.children.add(anchor); + anchor.click(); + anchor.remove(); + html.Url.revokeObjectUrl(url); +} From cb3597874d6ab1c54dc3d522b50ce07950fe6160 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Thu, 23 Jul 2026 09:42:14 +0200 Subject: [PATCH 2/2] docs: record responsable csv export --- review.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/review.md b/review.md index 6521361..bd043ee 100644 --- a/review.md +++ b/review.md @@ -39,7 +39,7 @@ l'historique complet, y compris des étapes devenues obsolètes après brancheme | Frontend étudiant | Fonctionnel pour la V1 | Parcours emprunt et restitution branchés sur l'API et testés en réel ; conservation de l'action avant identification à finaliser. | | Authentification | Simulée | `x-user-email` côté backend et identité démo côté frontend ; Azure AD reste à faire. | | API responsable | Terminé pour la V1 | Dashboard, emprunts, stock, anomalies, notifications, historique et export CSV, avec contrôle du rôle et du campus. | -| Frontend responsable | Fonctionnel pour la V1 | Espace de supervision responsive avec navigation dédiée, dashboard métier et vues API ; téléchargement CSV à finaliser. | +| Frontend responsable | Fonctionnel pour la V1 | Espace de supervision responsive avec navigation dédiée, dashboard métier, vues API et export CSV opérationnel. | | Tests automatisés | Non démarré | Tests backend/frontend/E2E à ajouter ; tests runtime manuels effectués. | | Documentation | Partielle | README principal et documents de conception présents ; OpenAPI, guides utilisateur et captures restent à produire. | @@ -423,6 +423,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Les listes métier partagent désormais une hiérarchie visuelle, une recherche et des lignes responsives cohérentes avec la charte ENSUP. - Aucun changement backend ni métier ; rendu validé manuellement dans le navigateur et `dart analyze` sans erreur. +### Étape 45 — Téléchargement CSV de l'historique responsable +- Branche dédiée `feat/responsable-historique-export` créée depuis `develop`. +- Client HTTP étendu pour récupérer un fichier binaire avec son type de contenu et son nom. +- Le bouton `Export CSV` appelle désormais l'endpoint responsable avec l'identité simulée de Karim et déclenche le téléchargement navigateur. +- Un état de chargement bloque les doubles clics et les erreurs API sont affichées dans l'interface. +- Endpoint vérifié au runtime : réponse `200`, type `text/csv`, nom `historique-responsable.csv` et contenu non vide. +- Vérification statique : `dart analyze` sans erreur. + --- -*Dernière mise à jour : 2026-07-22 — Espace responsable restructuré en interface de supervision premium et responsive.* +*Dernière mise à jour : 2026-07-23 — Export CSV de l'historique responsable opérationnel depuis l'interface.*