Modale admin AM (identité, dossier pro, enfants), API front avec repli, note back affiliation, et extraction gélule statut / liste enfants pour la fiche parent. Co-authored-by: Cursor <cursoragent@cursor.com>
113 lines
3.4 KiB
Dart
113 lines
3.4 KiB
Dart
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';
|
||
|
||
/// Cadre photo identité AM (35×45 mm) — même logique que [ValidationAmWizard].
|
||
class AdminAmPhotoFrame extends StatelessWidget {
|
||
final String? photoUrl;
|
||
|
||
static const double idPhotoAspectRatio = 35 / 45;
|
||
|
||
const AdminAmPhotoFrame({super.key, this.photoUrl});
|
||
|
||
/// Largeur colonne photo pour remplir [height] (cadre inclus).
|
||
static double columnWidthForHeight(double height) {
|
||
const frame = 16.0;
|
||
final innerH = (height - frame).clamp(0.0, double.infinity);
|
||
return innerH * idPhotoAspectRatio + frame;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final fullUrl = ApiConfig.absoluteMediaUrl(photoUrl);
|
||
return LayoutBuilder(
|
||
builder: (context, c) {
|
||
const uniformFrame = 8.0;
|
||
final maxPhotoW =
|
||
(c.maxWidth - 2 * uniformFrame).clamp(0.0, double.infinity);
|
||
final maxPhotoH =
|
||
(c.maxHeight - 2 * uniformFrame).clamp(0.0, double.infinity);
|
||
const ar = idPhotoAspectRatio;
|
||
|
||
double ph = maxPhotoH;
|
||
double pw = ph * ar;
|
||
if (pw > maxPhotoW) {
|
||
pw = maxPhotoW;
|
||
ph = pw / ar;
|
||
}
|
||
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey.shade100,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: Colors.grey.shade300),
|
||
),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(uniformFrame),
|
||
child: Align(
|
||
alignment: Alignment.topCenter,
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.circular(6),
|
||
child: SizedBox(
|
||
width: pw,
|
||
height: ph,
|
||
child: _photoContent(fullUrl, pw, ph),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _photoContent(String fullUrl, double pw, double ph) {
|
||
if (fullUrl.isEmpty) {
|
||
return ColoredBox(
|
||
color: Colors.grey.shade200,
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.person_off_outlined, size: 36, color: Colors.grey.shade400),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
'Aucune photo',
|
||
style: TextStyle(color: Colors.grey.shade600, fontSize: 11),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
return AuthNetworkImage(
|
||
url: fullUrl,
|
||
width: pw,
|
||
height: ph,
|
||
fit: BoxFit.cover,
|
||
loadingBuilder: (_, child, progress) {
|
||
if (progress == null) return child;
|
||
return ColoredBox(
|
||
color: Colors.grey.shade200,
|
||
child: Center(
|
||
child: SizedBox(
|
||
width: 22,
|
||
height: 22,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
value: progress.expectedTotalBytes != null
|
||
? progress.cumulativeBytesLoaded /
|
||
(progress.expectedTotalBytes!)
|
||
: null,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
errorBuilder: (_, __, ___) => ColoredBox(
|
||
color: Colors.grey.shade200,
|
||
child: Icon(Icons.broken_image_outlined, size: 36, color: Colors.grey.shade400),
|
||
),
|
||
);
|
||
}
|
||
}
|