import "package:flutter/material.dart"; import "package:google_fonts/google_fonts.dart"; import "../demo_identity.dart"; import "../services/responsable_service.dart"; import "../theme/ensup_colors.dart"; import "../widgets/brand_corners.dart"; import "../widgets/ensup_top_bar.dart"; class ResponsableScreen extends StatefulWidget { const ResponsableScreen({super.key}); @override State createState() => _ResponsableScreenState(); } class _ResponsableScreenState extends State { late Future<_ResponsableData> _future = _charger(); int _onglet = 0; bool _transitionEnCours = false; Future<_ResponsableData> _charger() async { final dashboard = await ResponsableService.dashboard(); final emprunts = await ResponsableService.emprunts(); final materiels = await ResponsableService.materiels(); final anomalies = await ResponsableService.anomalies(); final notifications = await ResponsableService.notifications(); final historique = await ResponsableService.historique(); return _ResponsableData( dashboard: dashboard, emprunts: emprunts, materiels: materiels, anomalies: anomalies, notifications: notifications, historique: historique, ); } void _rafraichir() { setState(() => _future = _charger()); } Future _changerStatut(Map anomalie) async { final prochain = _prochainStatut(anomalie["statut"] as String?); if (prochain == null || _transitionEnCours) { return; } setState(() => _transitionEnCours = true); try { await ResponsableService.changerStatutAnomalie( anomalie["id"] as int, prochain, ); _rafraichir(); } catch (error) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(error.toString()))); } finally { if (mounted) { setState(() => _transitionEnCours = false); } } } Future _marquerNotificationsLues() async { try { await ResponsableService.marquerNotificationsLues(); _rafraichir(); } catch (error) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(error.toString()))); } } String? _prochainStatut(String? statut) { return switch (statut) { "DETECTEE" => "EN_COURS_TRAITEMENT", "EN_COURS_TRAITEMENT" => "RESOLUE", "RESOLUE" => "CLOTUREE", _ => null, }; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: EnsupColors.soft, body: Stack( children: [ Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1320, maxHeight: 860), child: Container( margin: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: EnsupColors.text.withValues(alpha: 0.14), blurRadius: 60, offset: const Offset(0, 18), ), ], ), clipBehavior: Clip.antiAlias, child: Column( children: [ EnsupTopBar( brandText: "EME Responsable", campusTag: DemoIdentity.loginCampusTag, userName: "Karim Benali", userInitials: "KB", onBack: () => Navigator.of(context).pop(), ), Expanded( child: FutureBuilder<_ResponsableData>( future: _future, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center( child: CircularProgressIndicator(), ); } if (snapshot.hasError) { return _Erreur( message: snapshot.error.toString(), onRetry: _rafraichir, ); } return _contenu(snapshot.data!); }, ), ), ], ), ), ), ), const Positioned.fill(child: BrandCorners()), ], ), ); } Widget _contenu(_ResponsableData data) { final vues = [ _VueDashboard(data: data), _VueListe( titre: "Emprunts", items: data.emprunts, builder: _empruntTile, searchableText: (item) { final etudiant = Map.from(item["etudiant"] as Map); final materiel = Map.from(item["materiel"] as Map); return "${materiel["nom"]} ${materiel["reference"]} ${etudiant["prenom"]} ${etudiant["nom"]} ${item["statut"]}"; }, ), _VueListe( titre: "Stock", items: data.materiels, builder: _materielTile, searchableText: (item) { final categorie = Map.from(item["categorie"] as Map); return "${item["nom"]} ${item["reference"]} ${item["statut"]} ${categorie["nom"]}"; }, ), _VueListe( titre: "Anomalies", items: data.anomalies, builder: _anomalieTile, searchableText: (item) { final materiel = Map.from(item["materiel"] as Map); return "${item["type"]} ${item["description"]} ${item["statut"]} ${materiel["nom"]}"; }, ), _VueListe( titre: "Notifications", items: data.notifications, builder: _notificationTile, action: OutlinedButton.icon( onPressed: _marquerNotificationsLues, icon: const Icon(Icons.done_all, size: 16), label: const Text("Tout marquer lu"), ), searchableText: (item) => "${item["titre"]} ${item["message"]}", ), _VueListe( titre: "Historique", 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"), ), searchableText: (item) => "${item["action"]} ${item["description"]}", ), ]; return Column( children: [ _Onglets( index: _onglet, labels: const [ "Dashboard", "Emprunts", "Stock", "Anomalies", "Notifications", "Historique", ], onChanged: (index) => setState(() => _onglet = index), ), Expanded(child: vues[_onglet]), ], ); } Widget _empruntTile(Map item) { final etudiant = Map.from(item["etudiant"] as Map); final materiel = Map.from(item["materiel"] as Map); return _InfoTile( icon: Icons.assignment_outlined, title: "${materiel["nom"]}", subtitle: "${etudiant["prenom"]} ${etudiant["nom"]} · retour prévu ${_date(item["dateRetourPrevue"])}", meta: "Emprunt #${item["id"]}", trailing: _StatusBadge("${item["statut"]}"), ); } Widget _materielTile(Map item) { final categorie = Map.from(item["categorie"] as Map); return _InfoTile( icon: Icons.inventory_2_outlined, title: "${item["nom"]}", subtitle: "${item["reference"]} · ${categorie["nom"]} · ${item["etatGeneral"]}", meta: "${item["marque"]} ${item["modele"]}", trailing: _StatusBadge("${item["statut"]}"), ); } Widget _anomalieTile(Map item) { final materiel = Map.from(item["materiel"] as Map); final prochain = _prochainStatut(item["statut"] as String?); return _InfoTile( icon: Icons.report_problem_outlined, title: "${item["type"]} · ${materiel["nom"]}", subtitle: "${item["description"]}", meta: "Détectée le ${_date(item["dateDetection"])}", trailing: prochain == null ? _StatusBadge("${item["statut"]}") : FilledButton( onPressed: _transitionEnCours ? null : () => _changerStatut(item), child: Text(_statusLabel(prochain)), ), ); } Widget _notificationTile(Map item) { return _InfoTile( icon: item["lu"] == true ? Icons.notifications_none : Icons.notifications_active_outlined, title: "${item["titre"]}", subtitle: "${item["message"]}", meta: _date(item["dateCreation"]), trailing: _StatusBadge(item["lu"] == true ? "LUE" : "NON_LUE"), ); } Widget _historiqueTile(Map item) { final utilisateur = Map.from(item["utilisateur"] as Map); return _InfoTile( icon: Icons.history, title: _actionLabel("${item["action"]}"), subtitle: "${item["description"]}", meta: "${_date(item["dateAction"])} · ${utilisateur["prenom"]} ${utilisateur["nom"]}", trailing: const Icon(Icons.chevron_right, color: EnsupColors.muted), ); } String _date(Object? value) { if (value == null) return "-"; final parsed = DateTime.tryParse(value.toString()); if (parsed == null) return value.toString(); return "${parsed.day.toString().padLeft(2, "0")}/" "${parsed.month.toString().padLeft(2, "0")}/${parsed.year}"; } } String _statusLabel(String statut) { return switch (statut) { "DISPONIBLE" => "Disponible", "EMPRUNTE" => "Emprunté", "NON_CONFORME" => "Non conforme", "DETERIORE" => "Détérioré", "MAINTENANCE" => "Maintenance", "INDISPONIBLE" => "Indisponible", "EN_COURS" => "En cours", "EN_RETARD" => "En retard", "CLOTURE" => "Clôturé", "RETOUR_NON_CONFORME" => "Retour non conforme", "ANNULE" => "Annulé", "DETECTEE" => "À traiter", "EN_COURS_TRAITEMENT" => "En traitement", "RESOLUE" => "Résolue", "CLOTUREE" => "Clôturée", "LUE" => "Lue", "NON_LUE" => "Non lue", _ => statut.replaceAll("_", " ").toLowerCase(), }; } String _actionLabel(String action) { return switch (action) { "CREATION_EMPRUNT" => "Création d'emprunt", "CREATION_ANOMALIE" => "Création d'anomalie", "TRAITEMENT_ANOMALIE" => "Traitement d'anomalie", _ => action.replaceAll("_", " ").toLowerCase(), }; } Color _statusColor(String statut) { return switch (statut) { "DISPONIBLE" || "CLOTURE" || "RESOLUE" || "CLOTUREE" || "LUE" => EnsupColors.green, "EN_RETARD" || "RETOUR_NON_CONFORME" || "NON_CONFORME" || "DETECTEE" || "NON_LUE" => EnsupColors.red, "EN_COURS" || "EN_COURS_TRAITEMENT" || "EMPRUNTE" => EnsupColors.cyan, "MAINTENANCE" || "INDISPONIBLE" || "DETERIORE" => EnsupColors.purple, _ => EnsupColors.blue3, }; } class _ResponsableData { const _ResponsableData({ required this.dashboard, required this.emprunts, required this.materiels, required this.anomalies, required this.notifications, required this.historique, }); final Map dashboard; final List> emprunts; final List> materiels; final List> anomalies; final List> notifications; final List> historique; } class _VueDashboard extends StatelessWidget { const _VueDashboard({required this.data}); final _ResponsableData data; @override Widget build(BuildContext context) { final kpis = Map.from(data.dashboard["kpis"] as Map); return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Wrap( spacing: 14, runSpacing: 14, children: [ _KpiCard( label: "Matériels", value: _total(kpis["materiels"]), icon: Icons.inventory_2_outlined, ), _KpiCard( label: "Emprunts", value: _total(kpis["emprunts"]), icon: Icons.assignment_outlined, ), _KpiCard( label: "Anomalies", value: _total(kpis["anomalies"]), icon: Icons.report_problem_outlined, ), _KpiCard( label: "Notifications", value: "${kpis["notificationsNonLues"] ?? 0}", icon: Icons.notifications_outlined, ), ], ), ); } String _total(Object? repartition) { final map = Map.from(repartition as Map); return "${map["total"] ?? 0}"; } } class _VueListe extends StatefulWidget { const _VueListe({ required this.titre, required this.items, required this.builder, required this.searchableText, this.action, }); final String titre; final List> items; final Widget Function(Map) builder; final String Function(Map) searchableText; final Widget? action; @override State<_VueListe> createState() => _VueListeState(); } class _VueListeState extends State<_VueListe> { String _recherche = ""; @override Widget build(BuildContext context) { final terme = _recherche.trim().toLowerCase(); final resultats = terme.isEmpty ? widget.items : widget.items .where( (item) => widget.searchableText(item).toLowerCase().contains(terme), ) .toList(); return Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( "${widget.titre} (${resultats.length})", style: GoogleFonts.darkerGrotesque( fontSize: 26, fontWeight: FontWeight.w900, color: EnsupColors.text, ), ), ), if (widget.action != null) widget.action!, ], ), const SizedBox(height: 14), TextField( onChanged: (value) => setState(() => _recherche = value), decoration: InputDecoration( hintText: "Rechercher", prefixIcon: const Icon(Icons.search), isDense: true, filled: true, fillColor: EnsupColors.soft, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: EnsupColors.line), ), ), ), const SizedBox(height: 14), Expanded( child: resultats.isEmpty ? const _EmptyState() : ListView.separated( itemCount: resultats.length, separatorBuilder: (_, _) => const SizedBox(height: 10), itemBuilder: (_, index) => widget.builder(resultats[index]), ), ), ], ), ); } } class _Onglets extends StatelessWidget { const _Onglets({ required this.index, required this.labels, required this.onChanged, }); final int index; final List labels; final ValueChanged onChanged; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), decoration: const BoxDecoration( color: EnsupColors.soft, border: Border(bottom: BorderSide(color: EnsupColors.line)), ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: List.generate(labels.length, (i) { final selected = i == index; return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( selected: selected, label: Text(labels[i]), onSelected: (_) => onChanged(i), ), ); }), ), ), ); } } class _KpiCard extends StatelessWidget { const _KpiCard({ required this.label, required this.value, required this.icon, }); final String label; final String value; final IconData icon; @override Widget build(BuildContext context) { return Container( width: 210, padding: const EdgeInsets.all(18), decoration: BoxDecoration( border: Border.all(color: EnsupColors.line), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(icon, color: EnsupColors.blue3), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( value, style: GoogleFonts.darkerGrotesque( fontSize: 30, fontWeight: FontWeight.w900, ), ), Text( label, style: GoogleFonts.titilliumWeb( fontSize: 13, color: EnsupColors.muted, ), ), ], ), ], ), ); } } class _InfoTile extends StatelessWidget { const _InfoTile({ required this.icon, required this.title, required this.subtitle, required this.trailing, this.meta, }); final IconData icon; final String title; final String subtitle; final Widget trailing; final String? meta; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: EnsupColors.line), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(icon, color: EnsupColors.teal), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: GoogleFonts.darkerGrotesque( fontSize: 19, fontWeight: FontWeight.w800, ), ), Text(subtitle, maxLines: 2, overflow: TextOverflow.ellipsis), if (meta != null) ...[ const SizedBox(height: 4), Text( meta!, maxLines: 1, overflow: TextOverflow.ellipsis, style: GoogleFonts.titilliumWeb( fontSize: 12, color: EnsupColors.muted, ), ), ], ], ), ), const SizedBox(width: 12), trailing, ], ), ); } } class _StatusBadge extends StatelessWidget { const _StatusBadge(this.statut); final String statut; @override Widget build(BuildContext context) { final color = _statusColor(statut); return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(999), border: Border.all(color: color.withValues(alpha: 0.18)), ), child: Text( _statusLabel(statut), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: color, ), ), ); } } class _EmptyState extends StatelessWidget { const _EmptyState(); @override Widget build(BuildContext context) { return Center( child: Text( "Aucun résultat", style: GoogleFonts.titilliumWeb(color: EnsupColors.muted), ), ); } } class _Erreur extends StatelessWidget { const _Erreur({required this.message, required this.onRetry}); final String message; final VoidCallback onRetry; @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(message, textAlign: TextAlign.center), const SizedBox(height: 12), OutlinedButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), label: const Text("Réessayer"), ), ], ), ); } }