feat(frontend): download responsable history csv

This commit is contained in:
SaidSoighiri94
2026-07-23 09:42:01 +02:00
parent 95c9080777
commit c85fab8ca3
4 changed files with 120 additions and 11 deletions
@@ -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<ResponsableScreen> {
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<ResponsableScreen> {
}
}
Future<void> _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<ResponsableScreen> {
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"]}",
),
+55
View File
@@ -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<ApiFile> 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<dynamic> post(
String chemin,
Map<String, dynamic> 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;
}
}
@@ -29,6 +29,12 @@ class ResponsableService {
static Future<List<Map<String, dynamic>>> historique() =>
_liste("/responsable/historique");
static Future<ApiFile> exporterHistorique() => ApiClient.getFile(
"/responsable/historique/export.csv",
fallbackFileName: "historique-responsable.csv",
utilisateurEmail: _email,
);
static Future<int> marquerNotificationsLues() async {
final reponse = await ApiClient.patch(
"/responsable/notifications/lu-toutes",
+21
View File
@@ -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);
}