Files
petitspas/frontend/lib/widgets/quotidien/quotidien_bandeau.dart
T

283 lines
8.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/services/auth_service.dart';
import 'package:p_tits_pas/widgets/image_button.dart';
import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart';
/// Bandeau quotidien pastel : logo · pastilles Cahier de liaison / Agenda /
/// Contrat · menu user.
/// Réutilisable parent (#166) et AM (#169).
class QuotidienBandeau extends StatelessWidget {
final QuotidienNavSection selectedSection;
final ValueChanged<QuotidienNavSection> onSectionSelected;
final String userDisplayName;
final String? userEmail;
final VoidCallback? onProfileTap;
final VoidCallback? onSearchAmTap;
final VoidCallback? onSettingsTap;
final VoidCallback? onLogout;
const QuotidienBandeau({
super.key,
required this.selectedSection,
required this.onSectionSelected,
required this.userDisplayName,
this.userEmail,
this.onProfileTap,
this.onSearchAmTap,
this.onSettingsTap,
this.onLogout,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Row(
children: [
Image.asset(
QuotidienTheme.logoAsset,
height: 44,
fit: BoxFit.contain,
),
const Spacer(),
_NavPills(
selected: selectedSection,
onSelected: onSectionSelected,
),
const Spacer(),
_UserMenu(
displayName: userDisplayName,
email: userEmail,
onProfileTap: onProfileTap,
onSearchAmTap: onSearchAmTap,
onSettingsTap: onSettingsTap,
onLogout: onLogout,
),
],
),
);
}
}
class _NavPills extends StatelessWidget {
final QuotidienNavSection selected;
final ValueChanged<QuotidienNavSection> onSelected;
const _NavPills({
required this.selected,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_pill(
label: 'Cahier de liaison',
section: QuotidienNavSection.liaison,
),
const SizedBox(width: 10),
_pill(
label: 'Agenda',
section: QuotidienNavSection.agenda,
),
const SizedBox(width: 10),
_pill(
label: 'Contrat',
section: QuotidienNavSection.contrat,
),
],
);
}
Widget _pill({
required String label,
required QuotidienNavSection section,
}) {
final active = selected == section;
// Même famille que linscription : fonds « dessinés » (pas Material).
// Les deux pastilles ont la même silhouette (ratio ~4:1) : la largeur
// est dérivée de la hauteur pour ne jamais déformer le PNG.
// Chaque section garde sa couleur ; l'inactive est juste plus claire.
return ImageButton(
bg: QuotidienTheme.pillAssetFor(section),
bgOpacity: active ? 1.0 : QuotidienTheme.pillInactiveOpacity,
width: _pillHeight * QuotidienTheme.pillAspectRatio,
height: _pillHeight,
text: label,
textColor: QuotidienTheme.ink,
fontSize: 14,
onPressed: () => onSelected(section),
);
}
static const double _pillHeight = 46;
}
class _UserMenu extends StatelessWidget {
final String displayName;
final String? email;
final VoidCallback? onProfileTap;
final VoidCallback? onSearchAmTap;
final VoidCallback? onSettingsTap;
final VoidCallback? onLogout;
const _UserMenu({
required this.displayName,
this.email,
this.onProfileTap,
this.onSearchAmTap,
this.onSettingsTap,
this.onLogout,
});
@override
Widget build(BuildContext context) {
final shortName = displayName.trim().isEmpty ? 'Compte' : displayName.trim();
const double pillHeight = 46;
const double pillWidth = pillHeight * QuotidienTheme.pillAspectRatio;
return PopupMenuButton<String>(
tooltip: 'Menu utilisateur',
offset: const Offset(0, pillHeight + 4),
color: QuotidienTheme.ivory,
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: QuotidienTheme.lavender.withOpacity(0.6)),
),
onSelected: (value) async {
switch (value) {
case 'profile':
onProfileTap?.call();
break;
case 'search_am':
onSearchAmTap?.call();
break;
case 'settings':
onSettingsTap?.call();
break;
case 'logout':
await _confirmLogout(context);
break;
}
},
itemBuilder: (context) => [
if (email != null && email!.trim().isNotEmpty)
PopupMenuItem(
enabled: false,
child: Text(
email!,
style: TextStyle(color: QuotidienTheme.muted, fontSize: 12),
),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: 'profile',
child: ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.person_outline, size: 20),
title: Text('Profil'),
),
),
const PopupMenuItem(
value: 'search_am',
child: ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.search, size: 20),
title: Text('Recherche AM'),
),
),
const PopupMenuItem(
value: 'settings',
child: ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.settings_outlined, size: 20),
title: Text('Paramètres'),
),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: 'logout',
child: ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.logout, size: 20),
title: Text('Déconnexion'),
),
),
],
// Même pastille « dessinée » que la nav, teinte violet pastel charte.
child: Container(
width: pillWidth,
height: pillHeight,
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(QuotidienTheme.pillLavenderAsset),
fit: BoxFit.fill,
),
),
child: Row(
children: [
const Icon(
Icons.person_outline,
size: 20,
color: QuotidienTheme.ink,
),
const SizedBox(width: 6),
Expanded(
child: Text(
shortName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.merienda(
fontSize: 13,
fontWeight: FontWeight.bold,
color: QuotidienTheme.ink,
),
),
),
const Icon(
Icons.keyboard_arrow_down,
size: 18,
color: QuotidienTheme.ink,
),
],
),
),
);
}
Future<void> _confirmLogout(BuildContext context) async {
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Déconnexion'),
content: const Text('Voulez-vous vraiment vous déconnecter ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Déconnexion'),
),
],
),
);
if (ok != true) return;
onLogout?.call();
await AuthService.logout();
if (context.mounted) {
context.go('/login');
}
}
}