feat(am): inscription photo (picker, bytes web, photo_filename) (#120)

- ProfessionalInfoFormScreen : ImagePicker par défaut, MemoryImage, consentement si photo réelle
- AmRegistrationData : photoBytes + photoFilename ; récap étape 4
- registerAM : base64 + filename, erreurs réseau/JSON comme parents
- docs: 23_LISTE-TICKETS — issue #120

Made-with: Cursor
This commit is contained in:
2026-04-13 13:19:31 +02:00
parent cdae9b5f7c
commit 01c1be8f16
6 changed files with 147 additions and 60 deletions
@@ -1,7 +1,9 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'dart:math' as math;
import 'dart:io';
@@ -19,6 +21,8 @@ import 'custom_navigation_button.dart';
class ProfessionalInfoData {
final String? photoPath;
final File? photoFile;
final Uint8List? photoBytes;
final String? photoFilename;
final bool photoConsent;
final DateTime? dateOfBirth;
final String birthCity;
@@ -30,6 +34,8 @@ class ProfessionalInfoData {
ProfessionalInfoData({
this.photoPath,
this.photoFile,
this.photoBytes,
this.photoFilename,
this.photoConsent = false,
this.dateOfBirth,
this.birthCity = '',
@@ -86,8 +92,17 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
DateTime? _selectedDate;
String? _photoPathFramework;
File? _photoFile;
Uint8List? _photoBytes;
String? _photoFilename;
bool _photoConsent = false;
bool get _hasRealPhoto =>
(_photoBytes != null && _photoBytes!.isNotEmpty) ||
_photoFile != null ||
(_photoPathFramework != null &&
_photoPathFramework!.isNotEmpty &&
!_photoPathFramework!.startsWith('assets/'));
@override
void initState() {
super.initState();
@@ -106,6 +121,8 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
_capacityController.text = data.capacity?.toString() ?? '';
_photoPathFramework = data.photoPath;
_photoFile = data.photoFile;
_photoBytes = data.photoBytes;
_photoFilename = data.photoFilename;
_photoConsent = data.photoConsent;
}
}
@@ -140,18 +157,42 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
Future<void> _pickPhoto() async {
if (widget.onPickPhoto != null) {
await widget.onPickPhoto!();
} else {
// Comportement par défaut : utiliser un asset de test
setState(() {
_photoPathFramework = 'assets/images/icon_assmat.png';
_photoFile = null;
});
return;
}
final picker = ImagePicker();
try {
final XFile? picked = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 75,
maxWidth: 1200,
maxHeight: 1200,
);
if (picked == null) return;
final bytes = await picked.readAsBytes();
if (bytes.isEmpty) return;
File? file;
if (!kIsWeb) {
try {
final f = File(picked.path);
if (await f.exists()) file = f;
} catch (_) {}
}
final name = picked.name.trim();
setState(() {
_photoBytes = bytes;
_photoFile = file;
_photoPathFramework = null;
_photoFilename = name.isNotEmpty ? name : 'photo_am.jpg';
});
} catch (_) {}
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
if (_photoPathFramework != null && !_photoConsent) {
if (_hasRealPhoto && !_photoConsent) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Veuillez accepter le consentement photo pour continuer.')),
);
@@ -159,8 +200,10 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
}
final data = ProfessionalInfoData(
photoPath: _photoPathFramework,
photoPath: _photoBytes != null ? null : _photoPathFramework,
photoFile: _photoFile,
photoBytes: _photoBytes,
photoFilename: _photoFilename,
photoConsent: _photoConsent,
dateOfBirth: _selectedDate,
birthCity: _birthCityController.text,
@@ -169,7 +212,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
agrementNumber: _agrementController.text,
capacity: int.tryParse(_capacityController.text),
);
widget.onSubmit(data);
}
}
@@ -461,11 +504,14 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
),
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: _photoFile != null
? Image.file(_photoFile!, fit: BoxFit.cover)
: (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')
? Image.asset(_photoPathFramework!, fit: BoxFit.contain)
: Image.asset('assets/images/photo.png', fit: BoxFit.contain)),
child: _photoBytes != null && _photoBytes!.isNotEmpty
? Image.memory(_photoBytes!, fit: BoxFit.cover)
: _photoFile != null
? Image.file(_photoFile!, fit: BoxFit.cover)
: (_photoPathFramework != null &&
_photoPathFramework!.startsWith('assets/')
? Image.asset(_photoPathFramework!, fit: BoxFit.contain)
: Image.asset('assets/images/photo.png', fit: BoxFit.contain)),
),
),
),
@@ -741,7 +787,9 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
ImageProvider? currentImageProvider;
if (_photoFile != null) {
if (_photoBytes != null && _photoBytes!.isNotEmpty) {
currentImageProvider = MemoryImage(_photoBytes!);
} else if (_photoFile != null) {
currentImageProvider = FileImage(_photoFile!);
} else if (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')) {
currentImageProvider = AssetImage(_photoPathFramework!);
@@ -777,7 +825,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
AppCustomCheckbox(
label: 'J\'accepte l\'utilisation\nde ma photo.',
value: _photoConsent,
onChanged: (val) => setState(() => _photoConsent = val ?? false),
onChanged: (val) => setState(() => _photoConsent = val == true),
),
],
);