Files
geo/app/lib/presentation/widgets/passage_form_dialog.dart
pierre 599b9fcda0 feat: Gestion des secteurs et migration v3.0.4+304
- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 11:01:45 +02:00

1059 lines
40 KiB
Dart
Executable File

import 'package:flutter/material.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/operation_repository.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
import 'package:geosector_app/presentation/widgets/form_section.dart';
class PassageFormDialog extends StatefulWidget {
final PassageModel? passage;
final String title;
final bool readOnly;
final PassageRepository passageRepository;
final UserRepository userRepository;
final OperationRepository operationRepository;
final VoidCallback? onSuccess;
const PassageFormDialog({
super.key,
this.passage,
required this.title,
this.readOnly = false,
required this.passageRepository,
required this.userRepository,
required this.operationRepository,
this.onSuccess,
});
@override
State<PassageFormDialog> createState() => _PassageFormDialogState();
}
class _PassageFormDialogState extends State<PassageFormDialog> {
final _formKey = GlobalKey<FormState>();
bool _isSubmitting = false;
// Sélection du type de passage
int? _selectedPassageType;
bool _showForm = false;
// Controllers
late final TextEditingController _numeroController;
late final TextEditingController _rueBisController;
late final TextEditingController _rueController;
late final TextEditingController _villeController;
late final TextEditingController _nameController;
late final TextEditingController _emailController;
late final TextEditingController _phoneController;
late final TextEditingController _montantController;
late final TextEditingController _apptController;
late final TextEditingController _niveauController;
late final TextEditingController _residenceController;
late final TextEditingController _remarqueController;
late final TextEditingController _dateController;
late final TextEditingController _timeController;
// Form values
int _fkHabitat = 1; // Par défaut Maison
int _fkTypeReglement = 4; // Par défaut Non renseigné
DateTime _passedAt = DateTime.now(); // Date et heure de passage
// Helpers de validation
String? _validateNumero(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Le numéro est obligatoire';
}
final numero = int.tryParse(value.trim());
if (numero == null || numero <= 0) {
return 'Numéro invalide';
}
return null;
}
String? _validateRue(String? value) {
if (value == null || value.trim().isEmpty) {
return 'La rue est obligatoire';
}
if (value.trim().length < 3) {
return 'La rue doit contenir au moins 3 caractères';
}
return null;
}
String? _validateVille(String? value) {
if (value == null || value.trim().isEmpty) {
return 'La ville est obligatoire';
}
return null;
}
String? _validateNomOccupant(String? value) {
if (_selectedPassageType == 1) {
if (value == null || value.trim().isEmpty) {
return 'Le nom est obligatoire pour les passages effectués';
}
if (value.trim().length < 2) {
return 'Le nom doit contenir au moins 2 caractères';
}
}
return null;
}
String? _validateEmail(String? value) {
if (value == null || value.trim().isEmpty) {
return null; // Email optionnel
}
const emailRegex = r'^[^@]+@[^@]+\.[^@]+$';
if (!RegExp(emailRegex).hasMatch(value.trim())) {
return 'Format email invalide';
}
return null;
}
String? _validateMontant(String? value) {
if (_selectedPassageType == 1 || _selectedPassageType == 5) {
if (value == null || value.trim().isEmpty) {
return 'Le montant est obligatoire pour ce type';
}
final montant = double.tryParse(value.replaceAll(',', '.'));
if (montant == null) {
return 'Montant invalide';
}
if (montant <= 0) {
return 'Le montant doit être supérieur à 0';
}
}
return null;
}
@override
void initState() {
super.initState();
try {
debugPrint('=== DEBUT PassageFormDialog.initState ===');
// Initialize controllers with passage data if available
final passage = widget.passage;
debugPrint('Passage reçu: ${passage != null}');
if (passage != null) {
debugPrint('Passage ID: ${passage.id}');
debugPrint('Passage fkType: ${passage.fkType}');
debugPrint('Passage numero: ${passage.numero}');
debugPrint('Passage rueBis: ${passage.rueBis}');
debugPrint('Passage rue: ${passage.rue}');
debugPrint('Passage ville: ${passage.ville}');
debugPrint('Passage name: ${passage.name}');
debugPrint('Passage email: ${passage.email}');
debugPrint('Passage phone: ${passage.phone}');
debugPrint('Passage montant: ${passage.montant}');
debugPrint('Passage remarque: ${passage.remarque}');
debugPrint('Passage fkHabitat: ${passage.fkHabitat}');
debugPrint('Passage fkTypeReglement: ${passage.fkTypeReglement}');
}
_selectedPassageType = passage?.fkType;
_showForm = false; // Toujours commencer par la sélection de type
_fkHabitat = passage?.fkHabitat ?? 1;
_fkTypeReglement = passage?.fkTypeReglement ?? 4;
debugPrint('Initialisation des controllers...');
// S'assurer que toutes les valeurs null deviennent des chaînes vides
final String numero = passage?.numero.toString() ?? '';
final String rueBis = passage?.rueBis.toString() ?? '';
final String rue = passage?.rue.toString() ?? '';
final String ville = passage?.ville.toString() ?? '';
final String name = passage?.name.toString() ?? '';
final String email = passage?.email.toString() ?? '';
final String phone = passage?.phone.toString() ?? '';
// Pour le montant, afficher vide si c'est 0.00
final String montantRaw = passage?.montant.toString() ?? '0.00';
final String montant =
(montantRaw == '0.00' || montantRaw == '0' || montantRaw == '0.0')
? ''
: montantRaw;
final String appt = passage?.appt.toString() ?? '';
final String niveau = passage?.niveau.toString() ?? '';
final String residence = passage?.residence.toString() ?? '';
final String remarque = passage?.remarque.toString() ?? '';
// Initialiser la date de passage
_passedAt = passage?.passedAt ?? DateTime.now();
final String dateFormatted = '${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
final String timeFormatted = '${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
debugPrint('Valeurs pour controllers:');
debugPrint(' numero: "$numero"');
debugPrint(' rueBis: "$rueBis"');
debugPrint(' rue: "$rue"');
debugPrint(' ville: "$ville"');
debugPrint(' name: "$name"');
debugPrint(' email: "$email"');
debugPrint(' phone: "$phone"');
debugPrint(' montant: "$montant"');
debugPrint(' remarque: "$remarque"');
debugPrint(' passedAt: "$_passedAt"');
debugPrint(' dateFormatted: "$dateFormatted"');
debugPrint(' timeFormatted: "$timeFormatted"');
_numeroController = TextEditingController(text: numero);
_rueBisController = TextEditingController(text: rueBis);
_rueController = TextEditingController(text: rue);
_villeController = TextEditingController(text: ville);
_nameController = TextEditingController(text: name);
_emailController = TextEditingController(text: email);
_phoneController = TextEditingController(text: phone);
_montantController = TextEditingController(text: montant);
_apptController = TextEditingController(text: appt);
_niveauController = TextEditingController(text: niveau);
_residenceController = TextEditingController(text: residence);
_remarqueController = TextEditingController(text: remarque);
_dateController = TextEditingController(text: dateFormatted);
_timeController = TextEditingController(text: timeFormatted);
debugPrint('=== FIN PassageFormDialog.initState ===');
} catch (e, stackTrace) {
debugPrint('=== ERREUR PassageFormDialog.initState ===');
debugPrint('Erreur: $e');
debugPrint('StackTrace: $stackTrace');
rethrow;
}
}
@override
void dispose() {
_numeroController.dispose();
_rueBisController.dispose();
_rueController.dispose();
_villeController.dispose();
_nameController.dispose();
_emailController.dispose();
_phoneController.dispose();
_montantController.dispose();
_apptController.dispose();
_niveauController.dispose();
_residenceController.dispose();
_remarqueController.dispose();
_dateController.dispose();
_timeController.dispose();
super.dispose();
}
void _selectPassageType(int typeId) {
setState(() {
_selectedPassageType = typeId;
_showForm = true;
// Réinitialiser montant et type de règlement selon le type de passage
if (typeId == 1 || typeId == 5) {
// Pour "Effectué" (1) ou "Lot" (5), laisser le choix à l'utilisateur
// Ne pas forcer un type de règlement spécifique
} else {
// Pour tous les autres types, réinitialiser
_montantController.text = '';
_fkTypeReglement = 4; // Non renseigné
}
// Si c'est un nouveau passage et qu'on change de type, réinitialiser la date à maintenant
if (widget.passage == null) {
_passedAt = DateTime.now();
_dateController.text = '${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
_timeController.text = '${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
}
});
}
void _handleSubmit() async {
if (_isSubmitting) return;
// ✅ Validation intégrée avec focus automatique sur erreur
if (!_formKey.currentState!.validate()) {
// Le focus est automatiquement mis sur le premier champ en erreur
// Les bordures rouges et messages d'erreur sont affichés automatiquement
return;
}
setState(() {
_isSubmitting = true;
});
try {
final currentUser = widget.userRepository.getCurrentUser();
if (currentUser == null) {
throw Exception("Utilisateur non connecté");
}
final currentOperation = widget.operationRepository.getCurrentOperation();
if (currentOperation == null && widget.passage == null) {
throw Exception("Aucune opération active trouvée");
}
// Déterminer les valeurs de montant et type de règlement selon le type de passage
final String finalMontant =
(_selectedPassageType == 1 || _selectedPassageType == 5)
? _montantController.text.trim()
: '0';
// Déterminer le type de règlement final selon le type de passage
final int finalTypeReglement;
if (_selectedPassageType == 1 || _selectedPassageType == 5) {
// Pour les types 1 et 5, utiliser la valeur sélectionnée (qui a été validée)
finalTypeReglement = _fkTypeReglement;
} else {
// Pour tous les autres types, forcer "Non renseigné"
finalTypeReglement = 4;
}
final passageData = widget.passage?.copyWith(
fkType: _selectedPassageType!,
numero: _numeroController.text.trim(),
rueBis: _rueBisController.text.trim(),
rue: _rueController.text.trim(),
ville: _villeController.text.trim(),
name: _nameController.text.trim(),
email: _emailController.text.trim(),
phone: _phoneController.text.trim(),
montant: finalMontant,
fkHabitat: _fkHabitat,
appt: _apptController.text.trim(),
niveau: _niveauController.text.trim(),
residence: _residenceController.text.trim(),
remarque: _remarqueController.text.trim(),
fkTypeReglement: finalTypeReglement,
passedAt: _passedAt,
lastSyncedAt: DateTime.now(),
) ??
PassageModel(
id: 0, // Nouveau passage
fkOperation: currentOperation!.id, // Opération active
fkSector: 0, // Secteur par défaut
fkUser: currentUser.id, // Utilisateur actuel
fkType: _selectedPassageType!,
fkAdresse: "0", // Adresse par défaut pour nouveau passage
passedAt: _passedAt,
numero: _numeroController.text.trim(),
rue: _rueController.text.trim(),
rueBis: _rueBisController.text.trim(),
ville: _villeController.text.trim(),
residence: _residenceController.text.trim(),
fkHabitat: _fkHabitat,
appt: _apptController.text.trim(),
niveau: _niveauController.text.trim(),
gpsLat: '0.0', // GPS par défaut
gpsLng: '0.0', // GPS par défaut
nomRecu: _nameController.text.trim(),
remarque: _remarqueController.text.trim(),
montant: finalMontant,
fkTypeReglement: finalTypeReglement,
emailErreur: '',
nbPassages: 1,
name: _nameController.text.trim(),
email: _emailController.text.trim(),
phone: _phoneController.text.trim(),
lastSyncedAt: DateTime.now(),
isActive: true,
isSynced: false,
);
final success = widget.passage == null
? await widget.passageRepository.createPassage(passageData)
: await widget.passageRepository.updatePassage(passageData);
if (success && mounted) {
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
Navigator.of(context).pop();
widget.onSuccess?.call();
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
ApiException.showSuccess(
context,
widget.passage == null
? "Nouveau passage créé avec succès"
: "Passage modifié avec succès",
);
}
});
}
});
} else if (mounted) {
ApiException.showError(
context,
Exception(widget.passage == null
? "Échec de la création du passage"
: "Échec de la mise à jour du passage"),
);
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
if (mounted) {
setState(() {
_isSubmitting = false;
});
}
}
}
Widget _buildPassageTypeSelection() {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Type de passage',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 16),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: MediaQuery.of(context).size.width < 600 ? 1.8 : 2.5,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: AppKeys.typesPassages.length,
itemBuilder: (context, index) {
try {
final typeId = AppKeys.typesPassages.keys.elementAt(index);
final typeData = AppKeys.typesPassages[typeId];
if (typeData == null) {
debugPrint('ERREUR: typeData null pour typeId: $typeId');
return const SizedBox();
}
final isSelected = _selectedPassageType == typeId;
return InkWell(
onTap:
widget.readOnly ? null : () => _selectPassageType(typeId),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Color(typeData['couleur2'] as int? ?? 0xFF000000)
.withOpacity(0.15),
border: Border.all(
color: Color(typeData['couleur2'] as int? ?? 0xFF000000),
width: isSelected ? 3 : 2,
),
borderRadius: BorderRadius.circular(12),
boxShadow: isSelected
? [
BoxShadow(
color: Color(typeData['couleur2'] as int? ??
0xFF000000)
.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 2),
)
]
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
typeData['icon_data'] as IconData? ?? Icons.help,
color:
Color(typeData['couleur2'] as int? ?? 0xFF000000),
size: 36, // Icône plus grande
),
const SizedBox(height: 8),
Text(
typeData['titre'] as String? ?? 'Type inconnu',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight:
isSelected ? FontWeight.bold : FontWeight.w600,
color: isSelected
? Color(
typeData['couleur2'] as int? ?? 0xFF000000)
: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
],
),
),
);
} catch (e) {
debugPrint('ERREUR dans itemBuilder pour index $index: $e');
return const SizedBox();
}
},
),
],
);
}
Widget _buildPassageForm() {
try {
debugPrint('=== DEBUT _buildPassageForm ===');
final theme = Theme.of(context);
debugPrint('Building Form...');
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Date et Heure
FormSection(
title: 'Date et Heure de passage',
icon: Icons.schedule,
children: [
Row(
children: [
Expanded(
child: CustomTextField(
controller: _dateController,
label: "Date",
isRequired: true,
readOnly: true,
showLabel: false,
hintText: "DD/MM/YYYY",
suffixIcon: const Icon(Icons.calendar_today),
onTap: widget.readOnly ? null : _selectDate,
),
),
const SizedBox(width: 12),
Expanded(
child: CustomTextField(
controller: _timeController,
label: "Heure",
isRequired: true,
readOnly: true,
showLabel: false,
hintText: "HH:MM",
suffixIcon: const Icon(Icons.access_time),
onTap: widget.readOnly ? null : _selectTime,
),
),
],
),
],
),
const SizedBox(height: 24),
// Section Adresse
FormSection(
title: 'Adresse',
icon: Icons.location_on,
children: [
Row(
children: [
Expanded(
flex: 1,
child: CustomTextField(
controller: _numeroController,
label: "Numéro",
isRequired: true,
showLabel: false,
textAlign: TextAlign.right,
keyboardType: TextInputType.number,
readOnly: widget.readOnly,
validator: _validateNumero,
),
),
const SizedBox(width: 12),
Expanded(
flex: 1,
child: CustomTextField(
controller: _rueBisController,
label: "Bis, Ter...",
showLabel: false,
readOnly: widget.readOnly,
),
),
],
),
const SizedBox(height: 16),
CustomTextField(
controller: _rueController,
label: "Rue",
isRequired: true,
showLabel: false,
readOnly: widget.readOnly,
validator: _validateRue,
),
const SizedBox(height: 16),
CustomTextField(
controller: _villeController,
label: "Ville",
isRequired: true,
showLabel: false,
readOnly: widget.readOnly,
validator: _validateVille,
),
],
),
const SizedBox(height: 24),
// Section Habitat
FormSection(
title: 'Habitat',
icon: Icons.home,
children: [
// Boutons radio pour le type d'habitat
Row(
children: [
Expanded(
child: RadioListTile<int>(
title: const Text('Maison'),
value: 1,
groupValue: _fkHabitat,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_fkHabitat = value!;
});
},
contentPadding: EdgeInsets.zero,
),
),
Expanded(
child: RadioListTile<int>(
title: const Text('Appart'),
value: 2,
groupValue: _fkHabitat,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_fkHabitat = value!;
});
},
contentPadding: EdgeInsets.zero,
),
),
],
),
// Champs spécifiques aux appartements
if (_fkHabitat == 2) ...[
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 1,
child: TextField(
controller: _niveauController,
decoration: const InputDecoration(
labelText: "Niveau",
border: OutlineInputBorder(),
),
maxLength: 5,
readOnly: widget.readOnly,
),
),
const SizedBox(width: 12),
Expanded(
flex: 1,
child: TextField(
controller: _apptController,
decoration: const InputDecoration(
labelText: "Appt",
border: OutlineInputBorder(),
),
maxLength: 5,
readOnly: widget.readOnly,
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _residenceController,
decoration: const InputDecoration(
labelText: "Résidence",
border: OutlineInputBorder(),
),
maxLength: 50,
readOnly: widget.readOnly,
),
],
],
),
const SizedBox(height: 24),
// Section Occupant
FormSection(
title: 'Occupant',
icon: Icons.person,
children: [
CustomTextField(
controller: _nameController,
label: _selectedPassageType == 1
? "Nom de l'occupant"
: "Nom de l'occupant",
isRequired: _selectedPassageType == 1,
showLabel: false,
readOnly: widget.readOnly,
validator: _validateNomOccupant,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: CustomTextField(
controller: _emailController,
label: "Email",
showLabel: false,
keyboardType: TextInputType.emailAddress,
readOnly: widget.readOnly,
validator: _validateEmail,
prefixIcon: Icons.email,
),
),
const SizedBox(width: 12),
Expanded(
child: CustomTextField(
controller: _phoneController,
label: "Téléphone",
showLabel: false,
keyboardType: TextInputType.phone,
readOnly: widget.readOnly,
prefixIcon: Icons.phone,
),
),
],
),
],
),
const SizedBox(height: 24),
// Section Règlement et Remarque
FormSection(
title: (_selectedPassageType == 1 || _selectedPassageType == 5) ? 'Règlement et Note' : 'Note',
icon: Icons.note,
children: [
// Afficher montant et type de règlement seulement pour fkType 1 (Effectué) ou 5 (Lot)
if (_selectedPassageType == 1 || _selectedPassageType == 5) ...[
Row(
children: [
Expanded(
child: CustomTextField(
controller: _montantController,
label: "Montant",
isRequired: true,
showLabel: false,
hintText: "0.00",
textAlign: TextAlign.right,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
readOnly: widget.readOnly,
validator: _validateMontant,
prefixIcon: Icons.euro,
),
),
const SizedBox(width: 12),
Expanded(
child: DropdownButtonFormField<int>(
value: _fkTypeReglement,
decoration: const InputDecoration(
labelText: "Type de règlement *",
border: OutlineInputBorder(),
),
items: AppKeys.typesReglements.entries.map((entry) {
return DropdownMenuItem<int>(
value: entry.key,
child: Row(
children: [
Icon(
entry.value['icon_data'] as IconData,
color: Color(entry.value['couleur'] as int),
size: 16,
),
const SizedBox(width: 8),
Text(entry.value['titre'] as String),
],
),
);
}).toList(),
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_fkTypeReglement = value!;
});
},
validator: (_selectedPassageType == 1 || _selectedPassageType == 5)
? (value) {
if (value == null || value < 1 || value > 3) {
return 'Type de règlement requis';
}
return null;
}
: null,
),
),
],
),
const SizedBox(height: 16),
],
CustomTextField(
controller: _remarqueController,
label: "Note",
showLabel: false,
hintText: "Commentaire sur le passage...",
maxLines: 2,
readOnly: widget.readOnly,
),
],
),
],
),
);
} catch (e, stackTrace) {
debugPrint('=== ERREUR _buildPassageForm ===');
debugPrint('Erreur: $e');
debugPrint('StackTrace: $stackTrace');
return Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Icon(Icons.error, color: Colors.red),
const SizedBox(height: 8),
Text('Erreur dans le formulaire: $e'),
],
),
);
}
}
Future<void> _selectDate() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _passedAt,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
setState(() {
_passedAt = DateTime(
picked.year,
picked.month,
picked.day,
_passedAt.hour,
_passedAt.minute,
);
_dateController.text = '${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
});
}
}
Future<void> _selectTime() async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(_passedAt),
);
if (picked != null) {
setState(() {
_passedAt = DateTime(
_passedAt.year,
_passedAt.month,
_passedAt.day,
picked.hour,
picked.minute,
);
_timeController.text = '${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
});
}
}
@override
Widget build(BuildContext context) {
try {
debugPrint('=== DEBUT PassageFormDialog.build ===');
final theme = Theme.of(context);
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
insetPadding: const EdgeInsets.all(24),
child: Container(
width: MediaQuery.of(context).size.width * 0.6,
constraints: const BoxConstraints(
maxWidth: 800,
maxHeight: 900,
),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
decoration: BoxDecoration(
color: _selectedPassageType != null && AppKeys.typesPassages.containsKey(_selectedPassageType)
? Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000).withOpacity(0.1)
: null,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Icon(
widget.passage == null
? Icons.add_circle
: Icons.edit,
color: _selectedPassageType != null && AppKeys.typesPassages.containsKey(_selectedPassageType)
? Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000)
: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Flexible(
child: Text(
widget.title,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: _selectedPassageType != null && AppKeys.typesPassages.containsKey(_selectedPassageType)
? Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000)
: theme.colorScheme.primary,
),
overflow: TextOverflow.ellipsis,
),
),
if (_selectedPassageType != null && AppKeys.typesPassages.containsKey(_selectedPassageType)) ...[
const SizedBox(width: 12),
Icon(
AppKeys.typesPassages[_selectedPassageType]!['icon_data'] as IconData? ?? Icons.help,
color: Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000),
size: 20,
),
const SizedBox(width: 4),
Text(
AppKeys.typesPassages[_selectedPassageType]!['titre'] as String? ?? 'Inconnu',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000),
),
),
],
],
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: _isSubmitting
? null
: () => Navigator.of(context).pop(),
),
],
),
),
const Divider(),
// Contenu
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!_showForm) ...[
() {
debugPrint('Building passage type selection...');
return _buildPassageTypeSelection();
}(),
] else ...[
() {
debugPrint('Building passage form...');
return _buildPassageForm();
}(),
],
],
),
),
),
const SizedBox(height: 24),
// Footer
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isSubmitting
? null
: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
const SizedBox(width: 16),
if (!widget.readOnly &&
_showForm &&
_selectedPassageType != null)
ElevatedButton.icon(
onPressed: _isSubmitting ? null : _handleSubmit,
icon: _isSubmitting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(
widget.passage == null ? Icons.add : Icons.save),
label: Text(_isSubmitting
? 'Enregistrement...'
: (widget.passage == null ? 'Créer' : 'Enregistrer')),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
),
],
),
),
);
} catch (e, stackTrace) {
debugPrint('=== ERREUR PassageFormDialog.build ===');
debugPrint('Erreur: $e');
debugPrint('StackTrace: $stackTrace');
// Retourner un widget d'erreur simple
return Dialog(
child: Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error, color: Colors.red, size: 48),
const SizedBox(height: 16),
Text('Erreur lors de l\'affichage du formulaire: $e'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
),
);
}
}
}