Files
petitspas/frontend/lib/widgets/quotidien/couple_selector_bandeau.dart
T
2026-09-24 12:19:15 +02:00

385 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/models/couple_garde.dart';
import 'package:p_tits_pas/services/api/api_config.dart';
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart';
/// Rôle qui consulte le bandeau : côté parent (enfant | nounou) ou côté AM
/// (enfant | parent(s)). Le composant est le même, seule la lecture change.
/// Ticket #167 (parent) ; #170 réutilise en mode AM.
enum CoupleBandeauMode { parent, assistanteMaternelle }
/// Bandeau « couple de garde » en haut de la colonne gauche du TdB.
///
/// - Plusieurs couples → contrôle unique avec chevron (dropdown de bascule).
/// - Un seul couple → affichage informatif (pas de chevron, pas de menu).
/// - Aucun couple → état vide discret.
class CoupleSelectorBandeau extends StatelessWidget {
final CoupleBandeauMode mode;
final List<CoupleGarde> couples;
final String? selectedCoupleId;
final ValueChanged<CoupleGarde>? onCoupleSelected;
final bool loading;
final String? errorMessage;
final VoidCallback? onRetry;
const CoupleSelectorBandeau({
super.key,
this.mode = CoupleBandeauMode.parent,
required this.couples,
this.selectedCoupleId,
this.onCoupleSelected,
this.loading = false,
this.errorMessage,
this.onRetry,
});
CoupleGarde? get _selected {
if (couples.isEmpty) return null;
if (selectedCoupleId != null) {
for (final c in couples) {
if (c.id == selectedCoupleId) return c;
}
}
for (final c in couples) {
if (c.courant) return c;
}
return couples.first;
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
if (loading) return const _CoupleBandeauSkeleton();
if (errorMessage != null) {
return _CoupleBandeauError(message: errorMessage!, onRetry: onRetry);
}
if (couples.isEmpty) return const _CoupleBandeauEmpty();
final selected = _selected!;
final multi = couples.length > 1;
final card = _CoupleCard(
mode: mode,
couple: selected,
showChevron: multi,
colorIndex: couples.indexOf(selected),
);
if (!multi) return card;
return Theme(
data: Theme.of(context).copyWith(
hoverColor: Colors.transparent,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
focusColor: Colors.transparent,
),
child: PopupMenuButton<String>(
tooltip: 'Changer de garde',
// L'offset doit être suffisant pour descendre sous la carte (qui fait 90px).
offset: const Offset(0, 95),
color: Colors.transparent, // Le fond devient invisible
elevation: 0, // Pas d'ombre carrée
constraints: BoxConstraints.tightFor(width: constraints.maxWidth),
padding: EdgeInsets.zero,
onSelected: (id) {
final chosen = couples.firstWhere((c) => c.id == id);
onCoupleSelected?.call(chosen);
},
itemBuilder: (context) => [
for (final c in couples)
if (c.id != selected.id)
PopupMenuItem<String>(
value: c.id,
padding: EdgeInsets.zero,
height: 100, // Hauteur de la carte (90) + un peu de marge (10)
child: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _CoupleCard(
mode: mode,
couple: c,
showChevron: false,
selected: false,
isMenuItem: true,
colorIndex: couples.indexOf(c),
),
),
),
],
child: card,
),
);
});
}
}
/// Carte principale : enfant à gauche, séparateur, AM/parents à droite.
class _CoupleCard extends StatelessWidget {
final CoupleBandeauMode mode;
final CoupleGarde couple;
final bool showChevron;
final bool selected;
final bool isMenuItem;
final int colorIndex;
const _CoupleCard({
required this.mode,
required this.couple,
required this.showChevron,
this.selected = true,
this.isMenuItem = false,
required this.colorIndex,
});
@override
Widget build(BuildContext context) {
// Parent mode: enfant | AM
// AM mode: enfant | parent(s)
final isParentMode = mode == CoupleBandeauMode.parent;
final fallbackRoleIcon = isParentMode ? Icons.volunteer_activism : Icons.person_outline;
final fallbackRoleName = isParentMode ? 'Nounou' : 'Parent';
// TODO: In AM mode, we should ideally display "parent1+parent2 empilés" or centered.
// For now, CoupleGarde only gives us `am` (which in AM mode might just represent one parent, or the system needs to feed both parents here).
// Assuming backend will populate `am` field with the parent data when called by AM.
return Container(
height: 90,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(QuotidienTheme.bandeauColors[colorIndex % QuotidienTheme.bandeauColors.length]),
fit: BoxFit.fill,
),
),
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 12),
child: _MembreTile(
photoUrl: couple.enfant.photoUrl,
name: (couple.enfant.prenom != null && couple.enfant.prenom!.isNotEmpty)
? couple.enfant.prenom!
: couple.enfant.displayName(fallback: 'Enfant'),
fallbackIcon: Icons.child_care,
),
),
),
Container(
width: 8,
height: 44,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(QuotidienTheme.pencilLineVerticalAsset),
fit: BoxFit.contain,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: _MembreTile(
photoUrl: couple.am.photoUrl,
name: (couple.am.prenom != null && couple.am.prenom!.isNotEmpty)
? couple.am.prenom!
: couple.am.displayName(fallback: fallbackRoleName),
fallbackIcon: fallbackRoleIcon,
),
),
),
SizedBox(
width: 28,
child: showChevron
? const Icon(
Icons.keyboard_arrow_down,
color: QuotidienTheme.ink,
)
: null,
),
],
),
);
}
}
/// Photo ronde + nom (une moitié du couple).
class _MembreTile extends StatelessWidget {
final String? photoUrl;
final String name;
final IconData fallbackIcon;
const _MembreTile({
required this.photoUrl,
required this.name,
required this.fallbackIcon,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
_Avatar(photoUrl: photoUrl, fallbackIcon: fallbackIcon, size: 64),
const SizedBox(width: 12),
Expanded(
child: Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.merienda(
fontSize: 22,
fontWeight: FontWeight.bold,
color: QuotidienTheme.ink,
),
),
),
],
);
}
}
class _Avatar extends StatelessWidget {
final String? photoUrl;
final IconData fallbackIcon;
final double size;
const _Avatar({
required this.photoUrl,
required this.fallbackIcon,
required this.size,
});
@override
Widget build(BuildContext context) {
final url = ApiConfig.absoluteMediaUrl(photoUrl);
final placeholder = Container(
width: size,
height: size,
color: QuotidienTheme.lavender.withValues(alpha: 0.35),
child: Icon(fallbackIcon, size: size * 0.5, color: QuotidienTheme.ink),
);
return ClipOval(
child: SizedBox(
width: size,
height: size,
child: url.isEmpty
? placeholder
: AuthNetworkImage(
url: url,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => placeholder,
),
),
);
}
}
class _CoupleBandeauSkeleton extends StatelessWidget {
const _CoupleBandeauSkeleton();
@override
Widget build(BuildContext context) {
return Container(
height: 90,
decoration: BoxDecoration(
image: DecorationImage(
image: const AssetImage(QuotidienTheme.bandeauLime), // Un asset existant
fit: BoxFit.fill,
colorFilter: ColorFilter.mode(
Colors.white.withValues(alpha: 0.5),
BlendMode.lighten,
),
),
),
alignment: Alignment.center,
child: const SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
}
}
class _CoupleBandeauEmpty extends StatelessWidget {
const _CoupleBandeauEmpty();
@override
Widget build(BuildContext context) {
return Container(
height: 90,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(QuotidienTheme.bandeauLime), // Un asset existant par défaut
fit: BoxFit.fill,
),
),
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
const Icon(Icons.info_outline, color: QuotidienTheme.muted),
const SizedBox(width: 12),
Expanded(
child: Text(
'Aucune garde active pour le moment.',
style: GoogleFonts.merriweather(
fontSize: 14,
color: QuotidienTheme.muted,
),
),
),
],
),
);
}
}
class _CoupleBandeauError extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
const _CoupleBandeauError({required this.message, this.onRetry});
@override
Widget build(BuildContext context) {
return Container(
height: 90,
decoration: BoxDecoration(
image: DecorationImage(
image: const AssetImage(QuotidienTheme.bandeauLime), // Un asset existant
fit: BoxFit.fill,
colorFilter: ColorFilter.mode(
QuotidienTheme.coral.withValues(alpha: 0.3),
BlendMode.srcATop,
),
),
),
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
const Icon(Icons.error_outline, color: QuotidienTheme.ink),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: GoogleFonts.merriweather(
fontSize: 13,
color: QuotidienTheme.ink,
),
),
),
if (onRetry != null)
TextButton(
onPressed: onRetry,
child: const Text('Réessayer'),
),
],
),
);
}
}