feat(auth): restore authenticated user profile

This commit is contained in:
SaidSoighiri94
2026-07-23 14:39:35 +02:00
parent dc208fa13a
commit 36224f30ee
8 changed files with 276 additions and 76 deletions
@@ -23,7 +23,8 @@ class ConfirmationScreen extends StatelessWidget {
final dateEmprunt = emprunt.dateEmprunt;
final date =
"${_deuxChiffres(dateEmprunt.day)}/${_deuxChiffres(dateEmprunt.month)}/${dateEmprunt.year}";
final heure = "${_deuxChiffres(dateEmprunt.hour)}:${_deuxChiffres(dateEmprunt.minute)}";
final heure =
"${_deuxChiffres(dateEmprunt.hour)}:${_deuxChiffres(dateEmprunt.minute)}";
return Scaffold(
backgroundColor: EnsupColors.soft,
@@ -48,7 +49,7 @@ class ConfirmationScreen extends StatelessWidget {
clipBehavior: Clip.antiAlias,
child: Column(
children: [
const EnsupTopBar(
EnsupTopBar(
brandText: "EME · Confirmation",
campusTag: DemoIdentity.campusTag,
),
@@ -63,7 +64,10 @@ class ConfirmationScreen extends StatelessWidget {
),
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32),
padding: const EdgeInsets.symmetric(
horizontal: 28,
vertical: 32,
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(
@@ -75,10 +79,15 @@ class ConfirmationScreen extends StatelessWidget {
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () => Navigator.of(context).popUntil(
(route) => route.settings.name == "home",
onPressed: () =>
Navigator.of(context).popUntil(
(route) =>
route.settings.name == "home",
),
icon: const Icon(
Icons.home_outlined,
size: 18,
),
icon: const Icon(Icons.home_outlined, size: 18),
label: Text(
"Retour à l'accueil",
style: GoogleFonts.darkerGrotesque(
@@ -88,9 +97,13 @@ class ConfirmationScreen extends StatelessWidget {
),
style: FilledButton.styleFrom(
backgroundColor: EnsupColors.cyan,
padding: const EdgeInsets.symmetric(vertical: 16),
padding: const EdgeInsets.symmetric(
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(
10,
),
),
),
),
@@ -118,7 +131,9 @@ class ConfirmationScreen extends StatelessWidget {
width: double.infinity,
padding: const EdgeInsets.all(36),
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)]),
gradient: const LinearGradient(
colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFBBF7D0), width: 2),
),
@@ -129,7 +144,9 @@ class ConfirmationScreen extends StatelessWidget {
height: 72,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(colors: [Color(0xFF16A34A), Color(0xFF22C55E)]),
gradient: const LinearGradient(
colors: [Color(0xFF16A34A), Color(0xFF22C55E)],
),
boxShadow: [
BoxShadow(
color: const Color(0xFF16A34A).withValues(alpha: 0.3),
@@ -153,7 +170,10 @@ class ConfirmationScreen extends StatelessWidget {
Text(
"Votre emprunt a bien été enregistré. Vous pouvez récupérer le matériel.",
textAlign: TextAlign.center,
style: GoogleFonts.titilliumWeb(fontSize: 14, color: const Color(0xFF166534)),
style: GoogleFonts.titilliumWeb(
fontSize: 14,
color: const Color(0xFF166534),
),
),
],
),
@@ -191,7 +211,8 @@ class ConfirmationScreen extends StatelessWidget {
_rrow("Date d'emprunt", date),
_rrow("Heure", heure),
_rrow("Campus", item.campus),
if (note != null) _rrow("Note", note, valueColor: const Color(0xFFD97706)),
if (note != null)
_rrow("Note", note, valueColor: const Color(0xFFD97706)),
],
),
);
@@ -203,7 +224,13 @@ class ConfirmationScreen extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(cle, style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)),
Text(
cle,
style: GoogleFonts.titilliumWeb(
fontSize: 13,
color: EnsupColors.muted,
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
+1 -1
View File
@@ -71,7 +71,7 @@ class HomeScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ProfileCard(
ProfileCard(
name: DemoIdentity.userName,
subtitle: DemoIdentity.userSubtitle,
initials: DemoIdentity.userInitials,
+51 -36
View File
@@ -21,6 +21,14 @@ class LoginScreen extends StatefulWidget {
class _LoginScreenState extends State<LoginScreen> {
bool _connexionEnCours = false;
@override
void initState() {
super.initState();
if (AuthService.mode == AuthenticationMode.azure) {
WidgetsBinding.instance.addPostFrameCallback((_) => _restaurerSession());
}
}
void _ouvrirEtudiant() {
Navigator.of(context).push(
MaterialPageRoute(
@@ -39,7 +47,44 @@ class _LoginScreenState extends State<LoginScreen> {
);
}
void _identifierParQr() {
void _ouvrirPourRole(String roleCode) {
switch (roleCode) {
case "ETUDIANT":
_ouvrirEtudiant();
case "RESPONSABLE":
_ouvrirResponsable();
default:
throw StateError("Role utilisateur non autorise.");
}
}
Future<void> _restaurerSession() async {
setState(() => _connexionEnCours = true);
try {
if (!await AuthService.hasMicrosoftAccount()) {
return;
}
final reponse = await ApiClient.get("/auth/me");
final profile = AuthService.setProfileFromApi(reponse);
if (mounted) {
_ouvrirPourRole(profile.roleCode);
}
} catch (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("La session Microsoft doit etre renouvelee."),
),
);
} finally {
if (mounted) {
setState(() => _connexionEnCours = false);
}
}
}
Future<void> _identifierParQr() async {
if (_connexionEnCours) return;
if (AuthService.mode == AuthenticationMode.azure) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -52,8 +97,7 @@ class _LoginScreenState extends State<LoginScreen> {
return;
}
AuthService.selectDemoStudent();
_ouvrirEtudiant();
await _connecterMicrosoft(responsableDemo: false);
}
Future<void> _connecterMicrosoft({required bool responsableDemo}) async {
@@ -64,27 +108,17 @@ class _LoginScreenState extends State<LoginScreen> {
if (AuthService.mode == AuthenticationMode.demo) {
if (responsableDemo) {
AuthService.selectDemoResponsable();
_ouvrirResponsable();
} else {
AuthService.selectDemoStudent();
_ouvrirEtudiant();
}
return;
} else {
await AuthService.signInWithMicrosoft();
}
await AuthService.signInWithMicrosoft();
final reponse = await ApiClient.get("/auth/me");
final roleCode = _roleCode(reponse);
final profile = AuthService.setProfileFromApi(reponse);
if (!mounted) return;
switch (roleCode) {
case "ETUDIANT":
_ouvrirEtudiant();
case "RESPONSABLE":
_ouvrirResponsable();
default:
throw StateError("Role utilisateur non autorise.");
}
_ouvrirPourRole(profile.roleCode);
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(
@@ -97,25 +131,6 @@ class _LoginScreenState extends State<LoginScreen> {
}
}
String _roleCode(dynamic reponse) {
if (reponse is! Map || reponse["data"] is! Map) {
throw StateError("Profil utilisateur invalide.");
}
final data = reponse["data"] as Map;
if (data["role"] is! Map) {
throw StateError("Role utilisateur absent.");
}
final role = data["role"] as Map;
final code = role["code"];
if (code is! String) {
throw StateError("Role utilisateur invalide.");
}
return code;
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -1,7 +1,6 @@
import "package:flutter/material.dart";
import "package:google_fonts/google_fonts.dart";
import "../demo_identity.dart";
import "../services/auth_service.dart";
import "../services/responsable_service.dart";
import "../theme/ensup_colors.dart";
@@ -463,6 +462,12 @@ class _ResponsableTopBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final profile = AuthService.currentProfile;
final campusTag = profile?.campusTag ?? "Ensitech · Cergy";
final initials = profile?.initials ?? "KB";
final userName = profile?.fullName ?? "Karim Benali";
final roleLabel = profile?.roleLabel ?? "Responsable matériel";
return Container(
height: 68,
padding: const EdgeInsets.symmetric(horizontal: 20),
@@ -533,7 +538,7 @@ class _ResponsableTopBar extends StatelessWidget {
),
const SizedBox(width: 6),
Text(
DemoIdentity.loginCampusTag,
campusTag,
style: GoogleFonts.titilliumWeb(
fontSize: 12,
fontWeight: FontWeight.w600,
@@ -565,7 +570,7 @@ class _ResponsableTopBar extends StatelessWidget {
shape: BoxShape.circle,
),
child: Text(
"KB",
initials,
style: GoogleFonts.darkerGrotesque(
color: Colors.white,
fontSize: 13,
@@ -580,7 +585,7 @@ class _ResponsableTopBar extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Karim Benali",
userName,
style: GoogleFonts.titilliumWeb(
fontSize: 13,
fontWeight: FontWeight.w700,
@@ -588,7 +593,7 @@ class _ResponsableTopBar extends StatelessWidget {
),
),
Text(
"Responsable matériel",
roleLabel,
style: GoogleFonts.titilliumWeb(
fontSize: 11,
color: EnsupColors.muted,
@@ -10,7 +10,11 @@ import "../widgets/brand_corners.dart";
/// Résultat de la restitution (RG17-RG20) : conforme (succès) ou non conforme
/// (anomalie détectée + notification au responsable).
class ResultatRetourScreen extends StatelessWidget {
const ResultatRetourScreen({super.key, required this.emprunt, required this.anomalies});
const ResultatRetourScreen({
super.key,
required this.emprunt,
required this.anomalies,
});
final EmpruntItem emprunt;
final List<AnomalieRetour> anomalies;
@@ -48,7 +52,7 @@ class ResultatRetourScreen extends StatelessWidget {
clipBehavior: Clip.antiAlias,
child: Column(
children: [
const EnsupTopBar(
EnsupTopBar(
brandText: "EME · Résultat du contrôle",
campusTag: DemoIdentity.campusTag,
),
@@ -63,7 +67,10 @@ class ResultatRetourScreen extends StatelessWidget {
),
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32),
padding: const EdgeInsets.symmetric(
horizontal: 28,
vertical: 32,
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(
@@ -73,10 +80,15 @@ class ResultatRetourScreen extends StatelessWidget {
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () => Navigator.of(context).popUntil(
(route) => route.settings.name == "home",
onPressed: () =>
Navigator.of(context).popUntil(
(route) =>
route.settings.name == "home",
),
icon: const Icon(
Icons.home_outlined,
size: 18,
),
icon: const Icon(Icons.home_outlined, size: 18),
label: Text(
"Retour à l'accueil",
style: GoogleFonts.darkerGrotesque(
@@ -86,9 +98,13 @@ class ResultatRetourScreen extends StatelessWidget {
),
style: FilledButton.styleFrom(
backgroundColor: EnsupColors.cyan,
padding: const EdgeInsets.symmetric(vertical: 16),
padding: const EdgeInsets.symmetric(
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(
10,
),
),
),
),
@@ -116,7 +132,9 @@ class ResultatRetourScreen extends StatelessWidget {
width: double.infinity,
padding: const EdgeInsets.all(36),
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)]),
gradient: const LinearGradient(
colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFBBF7D0), width: 2),
),
@@ -127,7 +145,9 @@ class ResultatRetourScreen extends StatelessWidget {
height: 72,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(colors: [Color(0xFF16A34A), Color(0xFF22C55E)]),
gradient: LinearGradient(
colors: [Color(0xFF16A34A), Color(0xFF22C55E)],
),
),
child: const Icon(Icons.check, color: Colors.white, size: 36),
),
@@ -144,7 +164,10 @@ class ResultatRetourScreen extends StatelessWidget {
Text(
"L'emprunt a été clôturé et le matériel est de nouveau disponible.",
textAlign: TextAlign.center,
style: GoogleFonts.titilliumWeb(fontSize: 14, color: const Color(0xFF166534)),
style: GoogleFonts.titilliumWeb(
fontSize: 14,
color: const Color(0xFF166534),
),
),
],
),
@@ -164,12 +187,19 @@ class ResultatRetourScreen extends StatelessWidget {
decoration: BoxDecoration(
color: EnsupColors.red.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: EnsupColors.red.withValues(alpha: 0.2), width: 1.5),
border: Border.all(
color: EnsupColors.red.withValues(alpha: 0.2),
width: 1.5,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.warning_amber_rounded, color: EnsupColors.red, size: 24),
const Icon(
Icons.warning_amber_rounded,
color: EnsupColors.red,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Column(
@@ -227,7 +257,10 @@ class ResultatRetourScreen extends StatelessWidget {
anomalie.etatRetour,
valeurCouleur: EnsupColors.red,
),
_drow("Matériel", "${emprunt.materiel.nom} · ${emprunt.materiel.reference}"),
_drow(
"Matériel",
"${emprunt.materiel.nom} · ${emprunt.materiel.reference}",
),
_drow("Emprunteur", DemoIdentity.userName),
_drow("Date détection", date),
],
@@ -237,7 +270,11 @@ class ResultatRetourScreen extends StatelessWidget {
Text(
"Le responsable matériel traitera cette anomalie. Vous pouvez retourner à l'accueil.",
textAlign: TextAlign.center,
style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted, height: 1.6),
style: GoogleFonts.titilliumWeb(
fontSize: 13,
color: EnsupColors.muted,
height: 1.6,
),
),
],
);
@@ -249,7 +286,13 @@ class ResultatRetourScreen extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(cle, style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)),
Text(
cle,
style: GoogleFonts.titilliumWeb(
fontSize: 13,
color: EnsupColors.muted,
),
),
const SizedBox(width: 16),
Expanded(
child: Text(