Compare commits

..
Author SHA1 Message Date
jmartinandCursor ed3546d196 fix(#194): insert card_responses avec id_card explicite
TypeORM nullifiait id_card (conflit Column/JoinColumn) → 500 sur respond.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-25 10:52:36 +02:00
6 changed files with 32 additions and 109 deletions
+6 -8
View File
@@ -204,14 +204,12 @@ export class CardsService {
throw new BadRequestException('Motivation obligatoire en cas de refus');
}
await this.responsesRepo.save(
this.responsesRepo.create({
id_card: card.id,
id_utilisateur: userId,
action: dto.action,
comment: dto.comment?.trim(),
}),
);
await this.responsesRepo.insert({
id_card: card.id,
id_utilisateur: userId,
action: dto.action,
comment: dto.comment?.trim() || null,
});
if (card.id_absence) {
if (dto.action === CardResponseActionType.ACCEPT || dto.action === CardResponseActionType.ACK) {
+5 -14
View File
@@ -39,15 +39,13 @@ class CoupleMembre {
class CoupleGarde {
final String id;
final CoupleMembre enfant;
final CoupleMembre? am;
final List<CoupleMembre> parents;
final CoupleMembre am;
final bool courant;
const CoupleGarde({
required this.id,
required this.enfant,
this.am,
this.parents = const [],
required this.am,
this.courant = false,
});
@@ -57,15 +55,9 @@ class CoupleGarde {
enfant: CoupleMembre.fromJson(
Map<String, dynamic>.from(json['enfant'] ?? const {}),
),
am: json['am'] != null
? CoupleMembre.fromJson(Map<String, dynamic>.from(json['am']))
: null,
parents: json['parents'] != null
? (json['parents'] as List)
.whereType<Map>()
.map((e) => CoupleMembre.fromJson(Map<String, dynamic>.from(e)))
.toList()
: const [],
am: CoupleMembre.fromJson(
Map<String, dynamic>.from(json['am'] ?? const {}),
),
courant: json['courant'] == true,
);
}
@@ -75,7 +67,6 @@ class CoupleGarde {
id: id,
enfant: enfant,
am: am,
parents: parents,
courant: courant ?? this.courant,
);
}
@@ -45,7 +45,9 @@ class _AmDashboardScreenState extends State<AmDashboardScreen> {
_couplesError = null;
});
try {
final res = await CoupleGardeService.getAmCouplesGarde();
// TODO: Pour l'instant on utilise le service générique de test (CoupleGardeService.getCouplesGarde).
// Dans le futur, l'API pour l'AM (enfants_accueillis) fournira la vraie liste.
final res = await CoupleGardeService.getCouplesGarde();
if (!mounted) return;
setState(() {
_couples = res.couples;
@@ -65,7 +67,7 @@ class _AmDashboardScreenState extends State<AmDashboardScreen> {
if (couple.id == _selectedCoupleId) return;
setState(() => _selectedCoupleId = couple.id);
try {
final res = await CoupleGardeService.definirAmCoupleCourant(couple.id);
final res = await CoupleGardeService.definirCoupleCourant(couple.id);
if (!mounted) return;
setState(() {
_couples = res.couples;
+1 -5
View File
@@ -67,11 +67,7 @@ class ApiConfig {
static const String parentsCoupleGardeCourant =
'/parents/me/couples-garde/courant';
static const String assistantesMaternelles = '/assistantes-maternelles';
/// Couples de garde de l'AM connectée (#170 / #171).
static const String assistantesMaternellesCouplesGarde =
'/assistantes-maternelles/me/couples-garde';
static const String assistantesMaternellesCoupleGardeCourant =
'/assistantes-maternelles/me/couples-garde/courant';
/// Création dossier AM actif par le staff (#156) — body type register AM.
static const String assistantesMaternellesDossier =
'/assistantes-maternelles/dossier';
static const String enfants = '/enfants';
@@ -73,45 +73,4 @@ class CoupleGardeService {
Map<String, dynamic>.from(decoded as Map),
);
}
/// Liste les couples de garde et le couple courant de l'AM connectée.
static Future<CouplesGardeResponse> getAmCouplesGarde() async {
final response = await http.get(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternellesCouplesGarde}'),
headers: await _headers(),
);
if (response.statusCode != 200) {
throw Exception(
_extractError(response.body, 'Erreur chargement des couples de garde (AM)'),
);
}
final decoded = jsonDecode(response.body);
return CouplesGardeResponse.fromJson(
Map<String, dynamic>.from(decoded as Map),
);
}
/// Persiste le couple courant (AM) et renvoie la liste à jour.
static Future<CouplesGardeResponse> definirAmCoupleCourant(
String coupleId,
) async {
final response = await http.put(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternellesCoupleGardeCourant}'),
headers: await _headers(),
body: jsonEncode({'couple_id': coupleId}),
);
if (response.statusCode != 200) {
throw Exception(
_extractError(response.body, 'Erreur sélection du couple de garde (AM)'),
);
}
final decoded = jsonDecode(response.body);
return CouplesGardeResponse.fromJson(
Map<String, dynamic>.from(decoded as Map),
);
}
}
@@ -37,7 +37,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,46 +133,17 @@ 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) {
// Parent mode: enfant | AM (photo + prénom)
// AM mode: enfant | parent(s) — texte seul, pas de vignette parent
// Parent mode: enfant | AM
// AM mode: enfant | parent(s)
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,
),
);
}
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,
@@ -210,7 +181,13 @@ class _CoupleCard extends StatelessWidget {
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: amOrParentsWidget,
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(