375 lines
11 KiB
Dart
375 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) {
|
|
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: _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,
|
|
margin: const EdgeInsets.symmetric(horizontal: 12),
|
|
decoration: const BoxDecoration(
|
|
image: DecorationImage(
|
|
image: AssetImage(QuotidienTheme.pencilLineVerticalAsset),
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _MembreTile(
|
|
photoUrl: couple.am.photoUrl,
|
|
name: (couple.am.prenom != null && couple.am.prenom!.isNotEmpty)
|
|
? couple.am.prenom!
|
|
: couple.am.displayName(
|
|
fallback: mode == CoupleBandeauMode.parent
|
|
? 'Nounou'
|
|
: 'Parent',
|
|
),
|
|
fallbackIcon: mode == CoupleBandeauMode.parent
|
|
? Icons.volunteer_activism
|
|
: Icons.person_outline,
|
|
),
|
|
),
|
|
if (showChevron)
|
|
const Padding(
|
|
padding: EdgeInsets.only(left: 4),
|
|
child: Icon(
|
|
Icons.keyboard_arrow_down,
|
|
color: QuotidienTheme.ink,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|