release: squash merge develop → master

Quotidien / Epic C : feed bulles parent (#173), module Cartes (#194/#195),
evenements_agenda (#205), couples garde AM (#169–#171), API absences (#172).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-05 15:44:10 +02:00
co-authored by Cursor
parent ba6078feab
commit da024d997a
94 changed files with 6263 additions and 236 deletions
@@ -3,6 +3,7 @@ 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/carte.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
@@ -37,7 +38,7 @@ class CoupleSelectorBandeau extends StatelessWidget {
CoupleGarde? get _selected {
if (couples.isEmpty) return null;
if (selectedCoupleId != null) {
if (selectedCoupleId != null) {
for (final c in couples) {
if (c.id == selectedCoupleId) return c;
}
@@ -133,16 +134,51 @@ class _CoupleCard extends StatelessWidget {
required this.colorIndex,
});
/// Libellé parents côté AM : « Sophie » ou « Thomas et Claire ».
static String _parentsLabel(List<CoupleMembre> parents) {
if (parents.isEmpty) return 'Parents';
String prenomOf(CoupleMembre p) {
final prenom = (p.prenom ?? '').trim();
if (prenom.isNotEmpty) return prenom;
return p.displayName(fallback: 'Parent');
}
if (parents.length == 1) return prenomOf(parents.first);
return '${prenomOf(parents[0])} et ${prenomOf(parents[1])}';
}
@override
Widget build(BuildContext context) {
return Container(
height: 90,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(QuotidienTheme.bandeauColors[colorIndex % QuotidienTheme.bandeauColors.length]),
fit: BoxFit.fill,
// Parent mode: enfant | AM (photo + prénom)
// AM mode: enfant | parent(s) — texte seul, pas de vignette parent
final isParentMode = mode == CoupleBandeauMode.parent;
Widget amOrParentsWidget;
if (isParentMode) {
amOrParentsWidget = _MembreTile(
photoUrl: couple.am?.photoUrl,
name: (couple.am?.prenom != null && couple.am!.prenom!.isNotEmpty)
? couple.am!.prenom!
: (couple.am?.displayName(fallback: 'Nounou') ?? 'Nounou'),
fallbackIcon: Icons.volunteer_activism,
);
} else {
amOrParentsWidget = Text(
_parentsLabel(couple.parents),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.merienda(
fontSize: 22,
fontWeight: FontWeight.bold,
color: QuotidienTheme.ink,
),
),
);
}
return Carte.bandeau(
assetPath: QuotidienTheme
.bandeauColors[colorIndex % QuotidienTheme.bandeauColors.length],
height: 90,
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
@@ -151,7 +187,8 @@ class _CoupleCard extends StatelessWidget {
padding: const EdgeInsets.only(right: 12),
child: _MembreTile(
photoUrl: couple.enfant.photoUrl,
name: (couple.enfant.prenom != null && couple.enfant.prenom!.isNotEmpty)
name: (couple.enfant.prenom != null &&
couple.enfant.prenom!.isNotEmpty)
? couple.enfant.prenom!
: couple.enfant.displayName(fallback: 'Enfant'),
fallbackIcon: Icons.child_care,
@@ -171,29 +208,17 @@ class _CoupleCard extends StatelessWidget {
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: mode == CoupleBandeauMode.parent
? 'Nounou'
: 'Parent',
),
fallbackIcon: mode == CoupleBandeauMode.parent
? Icons.volunteer_activism
: Icons.person_outline,
),
child: amOrParentsWidget,
),
),
SizedBox(
width: 28,
child: showChevron
? const Icon(
Icons.keyboard_arrow_down,
color: QuotidienTheme.ink,
)
: null,
child: showChevron
? const Icon(
Icons.keyboard_arrow_down,
color: QuotidienTheme.ink,
)
: null,
),
],
),
@@ -279,24 +304,22 @@ class _CoupleBandeauSkeleton extends StatelessWidget {
@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,
return ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.white.withValues(alpha: 0.5),
BlendMode.lighten,
),
child: Carte.bandeau(
assetPath: QuotidienTheme.bandeauLime,
height: 90,
child: const Center(
child: SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
alignment: Alignment.center,
child: const SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
}
}
@@ -306,14 +329,9 @@ class _CoupleBandeauEmpty extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
return Carte.bandeau(
assetPath: QuotidienTheme.bandeauLime,
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: [
@@ -342,38 +360,35 @@ class _CoupleBandeauError extends StatelessWidget {
@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,
),
),
return ColorFiltered(
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,
child: Carte.bandeau(
assetPath: QuotidienTheme.bandeauLime,
height: 90,
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'),
),
],
if (onRetry != null)
TextButton(
onPressed: onRetry,
child: const Text('Réessayer'),
),
],
),
),
);
}