- Amélioration des interfaces utilisateur sur mobile - Optimisation de la responsivité des composants Flutter - Mise à jour des widgets de chat et communication - Amélioration des formulaires et tableaux - Ajout de nouveaux composants pour l'administration - Optimisation des thèmes et styles visuels 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1231 lines
44 KiB
Dart
Executable File
1231 lines
44 KiB
Dart
Executable File
import 'package:flutter/material.dart';
|
|
import 'package:geosector_app/core/theme/app_theme.dart';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
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, rootNavigator: false).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)
|
|
.withValues(alpha: 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)
|
|
.withValues(alpha: 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 ===');
|
|
|
|
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>(
|
|
initialValue: _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')}';
|
|
});
|
|
}
|
|
}
|
|
|
|
// Méthode pour détecter si on est sur mobile
|
|
bool _isMobile(BuildContext context) {
|
|
// Détecter si on est sur mobile natif ou web mobile (largeur < 600px)
|
|
return Theme.of(context).platform == TargetPlatform.iOS ||
|
|
Theme.of(context).platform == TargetPlatform.android ||
|
|
(kIsWeb && MediaQuery.of(context).size.width < 600);
|
|
}
|
|
|
|
// Méthode pour construire l'en-tête du formulaire
|
|
Widget _buildHeader() {
|
|
final theme = Theme.of(context);
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: _selectedPassageType != null &&
|
|
AppKeys.typesPassages.containsKey(_selectedPassageType)
|
|
? Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2']
|
|
as int? ??
|
|
0xFF000000)
|
|
.withValues(alpha: 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, rootNavigator: false).pop();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Méthode pour construire le contenu principal
|
|
Widget _buildContent() {
|
|
return SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (!_showForm) ...[
|
|
() {
|
|
debugPrint('Building passage type selection...');
|
|
return _buildPassageTypeSelection();
|
|
}(),
|
|
] else ...[
|
|
() {
|
|
debugPrint('Building passage form...');
|
|
return _buildPassageForm();
|
|
}(),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Méthode pour construire les boutons du footer
|
|
Widget _buildFooterButtons() {
|
|
final theme = Theme.of(context);
|
|
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: _isSubmitting ? null : () {
|
|
Navigator.of(context, rootNavigator: false).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,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Méthode pour construire le contenu du Dialog
|
|
Widget _buildDialogContent() {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Header
|
|
_buildHeader(),
|
|
const Divider(),
|
|
|
|
// Contenu
|
|
Expanded(
|
|
child: _buildContent(),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Footer
|
|
_buildFooterButtons(),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Méthode pour construire l'AppBar mobile
|
|
AppBar _buildMobileAppBar() {
|
|
final theme = Theme.of(context);
|
|
final typeColor = _selectedPassageType != null &&
|
|
AppKeys.typesPassages.containsKey(_selectedPassageType)
|
|
? Color(
|
|
AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ??
|
|
0xFF000000)
|
|
: theme.colorScheme.primary;
|
|
|
|
return AppBar(
|
|
backgroundColor: typeColor.withValues(alpha: 0.1),
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: Icon(Icons.close, color: typeColor),
|
|
onPressed: _isSubmitting ? null : () {
|
|
Navigator.of(context, rootNavigator: false).pop();
|
|
},
|
|
),
|
|
title: Row(
|
|
children: [
|
|
Icon(
|
|
widget.passage == null ? Icons.add_circle : Icons.edit,
|
|
color: typeColor,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
widget.title,
|
|
style: TextStyle(
|
|
color: typeColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppTheme.r(context, 18),
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: _selectedPassageType != null &&
|
|
AppKeys.typesPassages.containsKey(_selectedPassageType)
|
|
? [
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
AppKeys.typesPassages[_selectedPassageType]!['icon_data']
|
|
as IconData? ??
|
|
Icons.help,
|
|
color: typeColor,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
AppKeys.typesPassages[_selectedPassageType]!['titre']
|
|
as String? ??
|
|
'Inconnu',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: typeColor,
|
|
fontSize: AppTheme.r(context, 14),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
]
|
|
: null,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
try {
|
|
debugPrint('=== DEBUT PassageFormDialog.build ===');
|
|
|
|
final isMobile = _isMobile(context);
|
|
debugPrint('Platform mobile détectée: $isMobile');
|
|
|
|
if (isMobile) {
|
|
// Mode plein écran pour mobile
|
|
return Scaffold(
|
|
appBar: _buildMobileAppBar(),
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
child: _buildContent(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
bottomNavigationBar: _showForm && _selectedPassageType != null
|
|
? SafeArea(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
child: _buildFooterButtons(),
|
|
),
|
|
)
|
|
: null,
|
|
);
|
|
} else {
|
|
// Mode Dialog pour desktop/tablette
|
|
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: _buildDialogContent(),
|
|
),
|
|
);
|
|
}
|
|
} 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'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|