Files
geo/app/lib/presentation/widgets/btn_passages.dart
pierre 2f5946a184 feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles
- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD)
  * DEV: Clés TEST Pierre (mode test)
  * REC: Clés TEST Client (mode test)
  * PROD: Clés LIVE Client (mode live)
- Ajout de la gestion des bases de données immeubles/bâtiments
  * Configuration buildings_database pour DEV/REC/PROD
  * Service BuildingService pour enrichissement des adresses
- Optimisations pages et améliorations ergonomie
- Mises à jour des dépendances Composer
- Nettoyage des fichiers obsolètes

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 18:26:27 +01:00

380 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:geosector_app/app.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/services/current_user_service.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
/// Widget affichant 8 colonnes de statistiques de passages
class BtnPassages extends StatelessWidget {
final VoidCallback? onAddPassage;
/// Callback appelé lors du clic sur un type de passage
/// Si null, navigue vers /user/history (comportement par défaut)
/// Si fourni, appelle ce callback avec le typeId (ou null pour "Tous")
final Function(int? typeId)? onTypeSelected;
/// Type de passage actuellement sélectionné (pour l'indicateur visuel)
/// null = tous les passages
final int? selectedTypeId;
const BtnPassages({
super.key,
this.onAddPassage,
this.onTypeSelected,
this.selectedTypeId,
});
@override
Widget build(BuildContext context) {
// Récupérer l'utilisateur courant
final currentUser = userRepository.getCurrentUser();
final currentOpeUserId = currentUser?.opeUserId;
final currentOperation = userRepository.getCurrentOperation();
final isAdmin = CurrentUserService.instance.shouldShowAdminUI;
// Vérifier si le type Lot doit être affiché
final shouldShowLotType = _shouldShowLotType();
return SizedBox(
height: 80,
width: double.infinity,
child: ValueListenableBuilder<Box<PassageModel>>(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, box, child) {
// Filtrer les passages de l'opération courante
final allPassages = box.values.where((p) {
if (currentOperation == null) return false;
if (p.fkOperation != currentOperation.id) return false;
// Mode Admin : afficher tous les passages de l'opération
if (isAdmin) return true;
// Mode Membre : logique spéciale pour type 2 (À finaliser) : afficher tous
if (p.fkType == 2) return true;
// Mode Membre : autres types : seulement les passages de l'utilisateur
return p.fkUser == currentOpeUserId;
}).toList();
// Calculer les statistiques par type
final Map<int, int> countsByType = {};
int totalPassages = 0;
for (final passage in allPassages) {
countsByType[passage.fkType] = (countsByType[passage.fkType] ?? 0) + 1;
totalPassages++;
}
return Row(
children: [
// Colonne 1 : Total (non cliquable)
Expanded(
child: _buildTotalColumn(context, totalPassages),
),
const SizedBox(width: 2),
// Colonnes 2-7 : Types de passages (cliquables)
...AppKeys.typesPassages.entries.expand((entry) {
final typeId = entry.key;
final typeInfo = entry.value;
// Exclure le type Lot (5) si chkLotActif = false
if (typeId == 5 && !shouldShowLotType) {
return <Widget>[];
}
final count = countsByType[typeId] ?? 0;
final titre = typeInfo['titre'] as String;
final couleur = Color(typeInfo['couleur2'] as int);
final iconData = typeInfo['icon_data'] as IconData;
return <Widget>[
Expanded(
child: _buildTypeColumn(
context,
typeId,
titre,
count,
couleur,
iconData,
),
),
const SizedBox(width: 2),
];
}),
// Colonne 8 : Bouton + (nouveau passage)
Expanded(
child: _buildAddColumn(context),
),
],
);
},
),
);
}
/// Colonne TOTAL (cliquable, affiche tous les passages)
Widget _buildTotalColumn(BuildContext context, int total) {
final bool isSelected = selectedTypeId == null;
return InkWell(
onTap: () async {
if (onTypeSelected != null) {
// Mode callback : appeler le callback avec null (tous les passages)
onTypeSelected!(null);
} else {
// Mode navigation : sauvegarder dans Hive et naviguer
try {
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
await Hive.openBox(AppKeys.settingsBoxName);
}
final settingsBox = Hive.box(AppKeys.settingsBoxName);
await settingsBox.delete('history_selectedTypeId');
debugPrint('BtnPassages: Filtre type réinitialisé (tous les passages)');
} catch (e) {
debugPrint('Erreur réinitialisation filtre: $e');
}
// Navigation vers /history avec GoRouter (détection automatique admin/user)
if (context.mounted) {
final isAdmin = CurrentUserService.instance.shouldShowAdminUI;
context.go(isAdmin ? '/admin/history' : '/user/history');
}
}
},
child: Container(
height: 80,
decoration: BoxDecoration(
color: Colors.grey[200],
border: Border.all(
color: Colors.grey[400]!,
width: isSelected ? 5 : 1,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(AppTheme.borderRadiusMedium),
bottomLeft: Radius.circular(AppTheme.borderRadiusMedium),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.route,
size: 20,
color: Colors.black54,
),
const SizedBox(height: 2),
Text(
total.toString(),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 2),
Text(
total > 1 ? 'passages' : 'passage',
style: TextStyle(
fontSize: 10,
color: Colors.grey[700],
),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// Colonne TYPE DE PASSAGE (cliquable, navigue vers /history avec filtre)
Widget _buildTypeColumn(
BuildContext context,
int typeId,
String titre,
int count,
Color couleur,
IconData iconData,
) {
final bool isSelected = selectedTypeId == typeId;
return InkWell(
onTap: () async {
if (onTypeSelected != null) {
// Mode callback : appeler le callback avec le typeId
onTypeSelected!(typeId);
} else {
// Mode navigation : sauvegarder dans Hive et naviguer
try {
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
await Hive.openBox(AppKeys.settingsBoxName);
}
final settingsBox = Hive.box(AppKeys.settingsBoxName);
await settingsBox.put('history_selectedTypeId', typeId);
debugPrint('BtnPassages: Type $typeId sauvegardé dans Hive');
} catch (e) {
debugPrint('Erreur sauvegarde type: $e');
}
// Navigation vers /history avec GoRouter (détection automatique admin/user)
if (context.mounted) {
final isAdmin = CurrentUserService.instance.shouldShowAdminUI;
context.go(isAdmin ? '/admin/history' : '/user/history');
}
}
},
child: Container(
height: 80,
decoration: BoxDecoration(
color: couleur.withOpacity(0.1),
border: Border.all(
color: couleur,
width: isSelected ? 5 : 1,
),
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
iconData,
size: 20,
color: couleur,
),
const SizedBox(height: 2),
Text(
count.toString(),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: couleur,
),
),
const SizedBox(height: 2),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Text(
titre,
style: TextStyle(
fontSize: 10,
color: couleur,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
/// Colonne NOUVEAU PASSAGE (bouton +, fond vert)
Widget _buildAddColumn(BuildContext context) {
return InkWell(
onTap: () {
if (onAddPassage != null) {
onAddPassage!();
} else {
// Par défaut, ouvrir le dialogue de création
_showPassageFormDialog(context);
}
},
child: Container(
height: 80,
decoration: BoxDecoration(
color: AppTheme.buttonSuccessColor.withOpacity(0.1),
border: Border.all(
color: AppTheme.buttonSuccessColor,
width: 1,
),
borderRadius: const BorderRadius.only(
topRight: Radius.circular(AppTheme.borderRadiusMedium),
bottomRight: Radius.circular(AppTheme.borderRadiusMedium),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.add_circle_outline,
size: 24,
color: AppTheme.buttonSuccessColor,
),
const SizedBox(height: 2),
Text(
'Nouveau',
style: TextStyle(
fontSize: 10,
color: AppTheme.buttonSuccessColor,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// Vérifier si le type Lot doit être affiché
bool _shouldShowLotType() {
final currentUser = userRepository.getCurrentUser();
if (currentUser != null && currentUser.fkEntite != null) {
final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!);
if (userAmicale != null) {
return userAmicale.chkLotActif;
}
}
return true; // Par défaut, on affiche
}
/// Afficher le dialogue de création de passage
Future<void> _showPassageFormDialog(BuildContext context) async {
await showDialog<PassageModel>(
context: context,
builder: (context) => PassageFormDialog(
title: 'Nouveau passage',
readOnly: false,
passageRepository: passageRepository,
userRepository: userRepository,
operationRepository: operationRepository,
amicaleRepository: amicaleRepository,
onSuccess: () {
debugPrint('BtnPassages: Passage créé avec succès');
},
),
);
}
}