Files
petitspas/frontend/lib/widgets/admin/common/admin_user_card.dart
T
jmartinandCursor 0029c5ab86 fix(#153): peaufiner cartes dossiers et recherche.
Cartes neutres avec accent icône, photo AM si dispo, hint court
et infobulle de critères sur la barre de recherche.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 19:39:55 +02:00

197 lines
6.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:p_tits_pas/services/api/api_config.dart';
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
class AdminUserCard extends StatefulWidget {
final String title;
final List<String> subtitleLines;
final String? avatarUrl;
final IconData fallbackIcon;
final List<Widget> actions;
final Color? borderColor;
final Color? backgroundColor;
final Color? titleColor;
final Color? infoColor;
/// Fond du cercle avatar / icône (défaut lavande admin).
final Color? avatarBackgroundColor;
/// Couleur de licône fallback (défaut violet admin).
final Color? avatarIconColor;
final String? vigilanceTooltip;
final VoidCallback? onCardTap;
final EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry? contentPadding;
const AdminUserCard({
super.key,
required this.title,
required this.subtitleLines,
this.avatarUrl,
this.fallbackIcon = Icons.person,
this.actions = const [],
this.borderColor,
this.backgroundColor,
this.titleColor,
this.infoColor,
this.avatarBackgroundColor,
this.avatarIconColor,
this.vigilanceTooltip,
this.onCardTap,
this.margin,
this.contentPadding,
});
@override
State<AdminUserCard> createState() => _AdminUserCardState();
}
class _AdminUserCardState extends State<AdminUserCard> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
final infoLine =
widget.subtitleLines.where((e) => e.trim().isNotEmpty).join(' ');
final actionsWidth =
widget.actions.isNotEmpty ? widget.actions.length * 30.0 : 0.0;
final avatarUrl = ApiConfig.absoluteMediaUrl(widget.avatarUrl);
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
cursor: widget.onCardTap != null
? SystemMouseCursors.click
: MouseCursor.defer,
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(10),
child: InkWell(
onTap: widget.onCardTap,
borderRadius: BorderRadius.circular(10),
hoverColor: const Color(0x149CC5C0),
child: Card(
margin: widget.margin ?? const EdgeInsets.only(bottom: 12),
elevation: 0,
color: widget.backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: widget.borderColor ?? Colors.grey.shade300),
),
child: Padding(
padding: widget.contentPadding ??
const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
child: Row(
children: [
_buildAvatar(avatarUrl),
const SizedBox(width: 10),
if (widget.vigilanceTooltip != null) ...[
Tooltip(
message: widget.vigilanceTooltip!,
child: Icon(
Icons.error_outline,
size: 20,
color: Colors.orange.shade800,
),
),
const SizedBox(width: 6),
],
Expanded(
child: Row(
children: [
// flex: 0 → largeur du nom ; le reste va aux infos
// (évite le partage 50/50 qui tronque « Responsables »).
Flexible(
flex: 0,
fit: FlexFit.loose,
child: Text(
widget.title.isNotEmpty ? widget.title : 'Sans nom',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
).copyWith(color: widget.titleColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
infoLine,
style: const TextStyle(
color: Colors.black54,
fontSize: 12,
).copyWith(color: widget.infoColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
if (widget.actions.isNotEmpty)
SizedBox(
width: actionsWidth,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 120),
opacity: _isHovered ? 1 : 0,
child: IgnorePointer(
ignoring: !_isHovered,
child: IconTheme(
data: const IconThemeData(size: 17),
child: IconButtonTheme(
data: IconButtonThemeData(
style: IconButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
minimumSize: const Size(28, 28),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: widget.actions,
),
),
),
),
),
),
],
),
),
),
),
),
);
}
Widget _buildAvatar(String url) {
const size = 28.0;
final bg = widget.avatarBackgroundColor ?? const Color(0xFFEDE5FA);
final iconColor = widget.avatarIconColor ?? const Color(0xFF6B3FA0);
if (url.isEmpty) {
return CircleAvatar(
radius: 14,
backgroundColor: bg,
child: Icon(widget.fallbackIcon, size: 16, 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: Icon(widget.fallbackIcon, size: 16, color: iconColor),
),
),
),
);
}
}