- Panneau AM : validation NIR alignée API, date d’agrément requise côté app, capacité 1–10, n° agrément + date sur une ligne, ville/pays formatés au blur. - Widget RegistrationPhotoSlot (cadre, croix) partagé avec les cartes enfant. - AuthService : MIME PNG/JPEG pour la photo ; payload date_agrement. - Scripts register-am-dubois / mansouri ; chemins tests/ressources/photos ; doc test-data + seed ; smoke curl inscription AM. Made-with: Cursor
170 lines
5.4 KiB
Dart
170 lines
5.4 KiB
Dart
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'hover_relief_widget.dart';
|
|
|
|
/// Cadre « croquis » par-dessus la photo (même ressource que les cartes enfant).
|
|
const String kRegistrationPhotoFrameAsset = 'assets/images/photo_frame.png';
|
|
|
|
/// Indique si une photo « réelle » est présente (hors seul placeholder asset).
|
|
bool registrationPhotoSlotHasImage({
|
|
Uint8List? imageBytes,
|
|
File? imageFile,
|
|
String? imagePathOrAsset,
|
|
}) {
|
|
if (imageBytes != null && imageBytes.isNotEmpty) return true;
|
|
if (imageFile != null) return true;
|
|
if (imagePathOrAsset == null || imagePathOrAsset.isEmpty) return false;
|
|
return !imagePathOrAsset.startsWith('assets/');
|
|
}
|
|
|
|
/// Zone photo uniforme : cadre gris / placeholder, tap pour choisir, croix pour retirer, cadre dessin si photo.
|
|
///
|
|
/// Utilisé inscription **enfants** ([ChildCardWidget]) et **AM** ([ProfessionalInfoFormScreen]).
|
|
class RegistrationPhotoSlot extends StatelessWidget {
|
|
final double side;
|
|
final double scaleFactor;
|
|
final Uint8List? imageBytes;
|
|
final File? imageFile;
|
|
/// Chemin fichier local (hors `assets/`) ou URI asset `assets/...`.
|
|
final String? imagePathOrAsset;
|
|
final VoidCallback? onTapPick;
|
|
final VoidCallback? onClear;
|
|
final Color baseShadowColor;
|
|
final String placeholderAsset;
|
|
final bool isMobile;
|
|
|
|
const RegistrationPhotoSlot({
|
|
super.key,
|
|
required this.side,
|
|
this.scaleFactor = 1.0,
|
|
this.imageBytes,
|
|
this.imageFile,
|
|
this.imagePathOrAsset,
|
|
this.onTapPick,
|
|
this.onClear,
|
|
required this.baseShadowColor,
|
|
this.placeholderAsset = 'assets/images/photo.png',
|
|
this.isMobile = false,
|
|
});
|
|
|
|
bool get _hasImage => registrationPhotoSlotHasImage(
|
|
imageBytes: imageBytes,
|
|
imageFile: imageFile,
|
|
imagePathOrAsset: imagePathOrAsset,
|
|
);
|
|
|
|
Widget _buildImage({required BoxFit fit}) {
|
|
final b = imageBytes;
|
|
if (b != null && b.isNotEmpty) {
|
|
return Image.memory(b, fit: fit);
|
|
}
|
|
final f = imageFile;
|
|
if (f != null) {
|
|
return kIsWeb ? Image.network(f.path, fit: fit) : Image.file(f, fit: fit);
|
|
}
|
|
final p = imagePathOrAsset;
|
|
if (p != null && p.isNotEmpty) {
|
|
if (p.startsWith('assets/')) {
|
|
return Image.asset(p, fit: fit);
|
|
}
|
|
if (!kIsWeb) {
|
|
try {
|
|
final file = File(p);
|
|
if (file.existsSync()) {
|
|
return Image.file(file, fit: fit);
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
return Image.asset(placeholderAsset, fit: BoxFit.contain);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final canPick = onTapPick != null;
|
|
final canClear = onClear != null && _hasImage;
|
|
final outerRadius = BorderRadius.circular(10 * scaleFactor);
|
|
final photoClipRadius = BorderRadius.circular(side * 0.14);
|
|
final initialPhotoShadow =
|
|
Color.alphaBlend(Colors.black.withOpacity(0.22), baseShadowColor);
|
|
final hoverPhotoShadow =
|
|
Color.alphaBlend(Colors.black.withOpacity(0.32), baseShadowColor);
|
|
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
alignment: Alignment.center,
|
|
children: [
|
|
HoverReliefWidget(
|
|
onPressed: canPick ? onTapPick : null,
|
|
borderRadius: outerRadius,
|
|
initialElevation: 10,
|
|
hoverElevation: 16,
|
|
initialShadowColor: initialPhotoShadow,
|
|
hoverShadowColor: hoverPhotoShadow,
|
|
clipBehavior: _hasImage ? Clip.none : Clip.antiAlias,
|
|
child: SizedBox(
|
|
width: side,
|
|
height: side,
|
|
child: !_hasImage
|
|
? ClipRRect(
|
|
borderRadius: outerRadius,
|
|
child: Center(
|
|
child: Image.asset(
|
|
placeholderAsset,
|
|
fit: BoxFit.contain,
|
|
width: side,
|
|
height: side,
|
|
),
|
|
),
|
|
)
|
|
: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
Positioned.fill(
|
|
child: ClipRRect(
|
|
borderRadius: photoClipRadius,
|
|
child: _buildImage(fit: BoxFit.cover),
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
child: Image.asset(
|
|
kRegistrationPhotoFrameAsset,
|
|
fit: BoxFit.fill,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
const SizedBox.shrink(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (canClear)
|
|
Positioned(
|
|
top: 8 * scaleFactor,
|
|
right: 8 * scaleFactor,
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: onClear,
|
|
customBorder: const CircleBorder(),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(4),
|
|
child: Image.asset(
|
|
'assets/images/cross.png',
|
|
width: isMobile ? 26 : 30,
|
|
height: isMobile ? 26 : 30,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|