feat: Mise à jour des interfaces mobiles v3.2.3

- 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>
This commit is contained in:
2025-09-02 20:35:40 +02:00
parent 08f4bff358
commit 43d4cd66e1
2133 changed files with 237004 additions and 173303 deletions

View File

@@ -1,4 +1,6 @@
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';
@@ -184,8 +186,10 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
// 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')}';
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"');
@@ -258,12 +262,14 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
_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')}';
_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')}';
}
});
}
@@ -366,7 +372,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
if (success && mounted) {
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
Navigator.of(context).pop();
Navigator.of(context, rootNavigator: false).pop();
widget.onSuccess?.call();
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
@@ -420,7 +426,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: MediaQuery.of(context).size.width < 600 ? 1.8 : 2.5,
childAspectRatio:
MediaQuery.of(context).size.width < 600 ? 1.8 : 2.5,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
@@ -445,7 +452,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Color(typeData['couleur2'] as int? ?? 0xFF000000)
.withOpacity(0.15),
.withValues(alpha: 0.15),
border: Border.all(
color: Color(typeData['couleur2'] as int? ?? 0xFF000000),
width: isSelected ? 3 : 2,
@@ -456,7 +463,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
BoxShadow(
color: Color(typeData['couleur2'] as int? ??
0xFF000000)
.withOpacity(0.2),
.withValues(alpha: 0.2),
blurRadius: 8,
offset: const Offset(0, 2),
)
@@ -504,7 +511,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
Widget _buildPassageForm() {
try {
debugPrint('=== DEBUT _buildPassageForm ===');
final theme = Theme.of(context);
debugPrint('Building Form...');
return Form(
@@ -549,7 +555,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
],
),
const SizedBox(height: 24),
// Section Adresse
FormSection(
title: 'Adresse',
@@ -740,7 +746,9 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
// Section Règlement et Remarque
FormSection(
title: (_selectedPassageType == 1 || _selectedPassageType == 5) ? 'Règlement et Note' : 'Note',
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)
@@ -755,7 +763,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
showLabel: false,
hintText: "0.00",
textAlign: TextAlign.right,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
readOnly: widget.readOnly,
validator: _validateMontant,
prefixIcon: Icons.euro,
@@ -764,7 +773,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
const SizedBox(width: 12),
Expanded(
child: DropdownButtonFormField<int>(
value: _fkTypeReglement,
initialValue: _fkTypeReglement,
decoration: const InputDecoration(
labelText: "Type de règlement *",
border: OutlineInputBorder(),
@@ -792,7 +801,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
_fkTypeReglement = value!;
});
},
validator: (_selectedPassageType == 1 || _selectedPassageType == 5)
validator: (_selectedPassageType == 1 ||
_selectedPassageType == 5)
? (value) {
if (value == null || value < 1 || value > 3) {
return 'Type de règlement requis';
@@ -837,9 +847,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
}
}
Future<void> _selectDate() async {
final DateTime? picked = await showDatePicker(
context: context,
@@ -856,7 +863,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
_passedAt.hour,
_passedAt.minute,
);
_dateController.text = '${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
_dateController.text =
'${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
});
}
}
@@ -875,160 +883,324 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
picked.hour,
picked.minute,
);
_timeController.text = '${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
_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 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(),
final isMobile = _isMobile(context);
debugPrint('Platform mobile détectée: $isMobile');
// 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,
if (isMobile) {
// Mode plein écran pour mobile
return Scaffold(
appBar: _buildMobileAppBar(),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextButton(
onPressed: _isSubmitting
? null
: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
Expanded(
child: _buildContent(),
),
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,
),
),
],
),
],
),
),
),
);
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');