import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/parent_child_summary.dart'; import 'package:p_tits_pas/services/api/api_config.dart'; import 'package:p_tits_pas/utils/date_display_utils.dart'; import 'package:p_tits_pas/widgets/common/auth_network_image.dart'; /// Grille 2×2 des places d'accueil AM (max 4, limitée à [capacity]). class AmChildrenCapacityGrid extends StatelessWidget { static const int _gridSlots = 4; static const double _slotHeight = 44; static const double _gridPadding = 10; static const double _gridGap = 8; static const double _borderWidth = 1; static const double fixedHeight = _borderWidth * 2 + _gridPadding * 2 + _slotHeight * 2 + _gridGap; final List children; final int capacity; final void Function(ParentChildSummary child) onOpen; final void Function(ParentChildSummary child) onDetach; /// Clic sur une case libre → même flux que « Rattacher un enfant » (#149). final VoidCallback? onAttachEmpty; const AmChildrenCapacityGrid({ super.key, required this.children, required this.capacity, required this.onOpen, required this.onDetach, this.onAttachEmpty, }); @override Widget build(BuildContext context) { final maxSlots = capacity.clamp(0, _gridSlots); return Container( height: fixedHeight, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300, width: _borderWidth), ), padding: const EdgeInsets.all(_gridPadding), child: Column( children: [ _buildRow(0, 1, maxSlots), const SizedBox(height: _gridGap), _buildRow(2, 3, maxSlots), ], ), ); } Widget _buildRow(int leftIndex, int rightIndex, int maxSlots) { return Row( children: [ Expanded(child: _buildSlot(leftIndex, maxSlots)), const SizedBox(width: _gridGap), Expanded(child: _buildSlot(rightIndex, maxSlots)), ], ); } Widget _buildSlot(int index, int maxSlots) { return SizedBox( height: _slotHeight, child: _buildSlotContent(index, maxSlots), ); } Widget _buildSlotContent(int index, int maxSlots) { if (index < children.length) { return _OccupiedSlot( child: children[index], overCapacity: index >= maxSlots, onOpen: () => onOpen(children[index]), onDetach: () => onDetach(children[index]), ); } if (index < maxSlots) { return _EmptySlot(onTap: onAttachEmpty); } return const _UnavailableSlot(); } } class _SlotShell extends StatelessWidget { final Color backgroundColor; final Color borderColor; final Widget child; const _SlotShell({ required this.backgroundColor, required this.borderColor, required this.child, }); @override Widget build(BuildContext context) { return SizedBox.expand( child: DecoratedBox( decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(8), border: Border.all(color: borderColor), ), child: child, ), ); } } class _UnavailableSlot extends StatelessWidget { const _UnavailableSlot(); @override Widget build(BuildContext context) { return _SlotShell( backgroundColor: Colors.grey.shade100, borderColor: Colors.grey.shade200, child: Center( child: Icon( Icons.block_outlined, size: 20, color: Colors.grey.shade400, ), ), ); } } class _EmptySlot extends StatefulWidget { final VoidCallback? onTap; const _EmptySlot({this.onTap}); @override State<_EmptySlot> createState() => _EmptySlotState(); } class _EmptySlotState extends State<_EmptySlot> { bool _hovered = false; @override Widget build(BuildContext context) { final clickable = widget.onTap != null; return MouseRegion( onEnter: clickable ? (_) => setState(() => _hovered = true) : null, onExit: clickable ? (_) => setState(() => _hovered = false) : null, cursor: clickable ? SystemMouseCursors.click : MouseCursor.defer, child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( onTap: widget.onTap, borderRadius: BorderRadius.circular(8), hoverColor: const Color(0x149CC5C0), child: _SlotShell( backgroundColor: _hovered ? const Color(0xFFF3F0FA) : Colors.grey.shade50, borderColor: _hovered ? const Color(0xFFB8A4D4) : Colors.grey.shade300, child: Center( child: Text( 'Place libre', style: TextStyle( fontSize: 12, color: _hovered ? const Color(0xFF6B3FA0) : Colors.grey.shade500, fontWeight: _hovered ? FontWeight.w600 : FontWeight.w400, ), ), ), ), ), ), ); } } class _OccupiedSlot extends StatefulWidget { final ParentChildSummary child; final bool overCapacity; final VoidCallback onOpen; final VoidCallback onDetach; const _OccupiedSlot({ required this.child, required this.overCapacity, required this.onOpen, required this.onDetach, }); @override State<_OccupiedSlot> createState() => _OccupiedSlotState(); } class _OccupiedSlotState extends State<_OccupiedSlot> { bool _hovered = false; @override Widget build(BuildContext context) { final age = formatChildAgeLabel( birthDate: widget.child.birthDate, dueDate: widget.child.dueDate, status: widget.child.status, ); final avatarUrl = ApiConfig.absoluteMediaUrl(widget.child.photoUrl ?? ''); return MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: _SlotShell( backgroundColor: widget.overCapacity ? Colors.red.shade50 : const Color(0xFFF8F5FC), borderColor: widget.overCapacity ? Colors.red.shade300 : const Color(0xFFD8CCE8), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ _buildAvatar(avatarUrl), const SizedBox(width: 8), Expanded( child: Row( children: [ Flexible( fit: FlexFit.loose, child: Text( widget.child.fullName, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (age.isNotEmpty) ...[ const SizedBox(width: 8), Expanded( child: Text( age, style: const TextStyle( fontSize: 11, color: Colors.black54, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ], ), ), AnimatedOpacity( duration: const Duration(milliseconds: 120), opacity: _hovered ? 1 : 0, child: IgnorePointer( ignoring: !_hovered, child: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( tooltip: 'Voir / modifier', icon: const Icon(Icons.visibility_outlined, size: 20), padding: const EdgeInsets.all(4), constraints: const BoxConstraints( minWidth: 32, minHeight: 32, ), onPressed: widget.onOpen, ), IconButton( tooltip: 'Détacher', icon: Icon( Icons.link_off, size: 20, color: Colors.orange.shade800, ), padding: const EdgeInsets.all(4), constraints: const BoxConstraints( minWidth: 32, minHeight: 32, ), onPressed: widget.onDetach, ), ], ), ), ), ], ), ), ), ); } Widget _buildAvatar(String url) { const size = 28.0; const bg = Color(0xFFEDE5FA); const iconColor = Color(0xFF6B3FA0); if (url.isEmpty) { return CircleAvatar( radius: 14, backgroundColor: bg, child: const Icon(Icons.child_care_outlined, size: 15, color: iconColor), ); } return ClipOval( child: SizedBox( width: size, height: size, child: AuthNetworkImage( url: url, width: size, height: size, fit: BoxFit.cover, errorBuilder: (_, __, ___) => ColoredBox( color: bg, child: const Icon(Icons.child_care_outlined, size: 15, color: iconColor), ), ), ), ); } }