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>
This commit is contained in:
pierre
2025-11-09 18:26:27 +01:00
parent 21657a3820
commit 2f5946a184
812 changed files with 142105 additions and 25992 deletions

View File

@@ -218,21 +218,21 @@ class _PassageFormState extends State<PassageForm> {
decoration: InputDecoration(
hintText: '0.00 €',
hintStyle: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
fillColor: const Color(0xFFF4F5F6),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: theme.colorScheme.onSurface.withValues(alpha: 0.1),
color: theme.colorScheme.onSurface.withOpacity(0.1),
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: theme.colorScheme.onSurface.withValues(alpha: 0.1),
color: theme.colorScheme.onSurface.withOpacity(0.1),
width: 1,
),
),
@@ -360,10 +360,10 @@ class _PassageFormState extends State<PassageForm> {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
decoration: BoxDecoration(
color: const Color(0xFFF4F5F6).withValues(alpha: 0.85),
color: const Color(0xFFF4F5F6).withOpacity(0.85),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF20335E).withValues(alpha: 0.1),
color: const Color(0xFF20335E).withOpacity(0.1),
width: 1,
),
),

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:intl/intl.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/payment_link_result.dart';
import 'package:geosector_app/presentation/widgets/qr_code_payment_dialog.dart';
/// Un widget réutilisable pour afficher une liste de passages (affichage pur)
class PassagesListWidget extends StatelessWidget {
@@ -35,6 +37,9 @@ class PassagesListWidget extends StatelessWidget {
/// Callback appelé lorsque le bouton d'ajout est cliqué
final VoidCallback? onAddPassage;
/// Type de passage filtré (optionnel, pour affichage dans le titre)
final String? filteredPassageType;
const PassagesListWidget({
super.key,
required this.passages,
@@ -47,6 +52,7 @@ class PassagesListWidget extends StatelessWidget {
this.onDetailsView,
this.onPassageDelete,
this.onAddPassage,
this.filteredPassageType,
});
@override
@@ -81,7 +87,7 @@ class PassagesListWidget extends StatelessWidget {
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Color.alphaBlend(
theme.colorScheme.primary.withValues(alpha: 0.1),
theme.colorScheme.primary.withOpacity(0.1),
theme.colorScheme.surface,
),
),
@@ -91,13 +97,13 @@ class PassagesListWidget extends StatelessWidget {
Row(
children: [
Icon(
Icons.list_alt,
Icons.route,
size: 20,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'${passages.length} passage${passages.length > 1 ? 's' : ''}',
_buildPassageCountText(),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
@@ -200,23 +206,37 @@ class PassagesListWidget extends StatelessWidget {
'icon_data': Icons.help_outline,
};
// Récupérer nbPassages pour le type 2
final nbPassages = passage['nb_passages'] as int? ?? passage['nbPassages'] as int? ?? 0;
// Récupérer la couleur de fond selon le type et nbPassages
Color backgroundColor;
Color iconColor;
bool useOutlinedIcon = false;
if (typeId == 2) {
// Type 2 (À finaliser) : adapter la couleur selon nbPassages
final nbPassages = passage['nbPassages'] as int? ?? passage['nb_passages'] as int? ?? 0;
if (nbPassages == 0) {
backgroundColor = Color(typeInfo['couleur1'] as int? ?? 0xFFFFFFFF);
iconColor = Color(typeInfo['couleur2'] as int? ?? 0xFFFFB978);
useOutlinedIcon = true; // Utiliser l'icône outlined pour la visibilité
} else if (nbPassages == 1) {
backgroundColor = Color(typeInfo['couleur2'] as int? ?? 0xFFF7A278);
backgroundColor = Color(typeInfo['couleur2'] as int? ?? 0xFFFFB978);
iconColor = backgroundColor;
useOutlinedIcon = false;
} else {
// nbPassages > 1
backgroundColor = Color(typeInfo['couleur3'] as int? ?? 0xFFE65100);
backgroundColor = Color(typeInfo['couleur3'] as int? ?? 0xFFE66F00);
iconColor = backgroundColor;
useOutlinedIcon = false;
}
} else {
// Autres types : utiliser couleur2 par défaut
backgroundColor = Color(typeInfo['couleur2'] as int? ?? 0xFF9E9E9E);
iconColor = backgroundColor;
useOutlinedIcon = false;
}
final typeIcon = typeInfo['icon_data'] as IconData? ?? Icons.help_outline;
// Informations du passage
@@ -291,13 +311,13 @@ class PassagesListWidget extends StatelessWidget {
height: 50,
width: 50,
decoration: BoxDecoration(
color: backgroundColor.withValues(alpha: 0.5),
color: backgroundColor.withOpacity(0.5),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
typeIcon,
useOutlinedIcon ? Icons.refresh_outlined : typeIcon,
size: 28,
color: backgroundColor.withValues(alpha: 1.0),
color: iconColor.withOpacity(1.0),
),
),
const SizedBox(width: 12),
@@ -308,23 +328,47 @@ class PassagesListWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Ligne 1 : Date (si définie) + Actions à droite
// Ligne 1 : Date (si définie) + Nom + Actions à droite
Row(
children: [
// Date (si définie)
if (formattedDate != null)
Expanded(
child: Text(
formattedDate,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
)
else
const Spacer(),
// Date et nom
Expanded(
child: Row(
children: [
// Date (si définie)
if (formattedDate != null)
Text(
formattedDate,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
// Nom du passage (si défini)
if (passage['name'] != null &&
(passage['name'] as String).trim().isNotEmpty) ...[
if (formattedDate != null)
Text(
' - ',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
Flexible(
child: Text(
passage['name'] as String,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
// Actions
if (showActions) ...[
@@ -343,7 +387,7 @@ class PassagesListWidget extends StatelessWidget {
),
const SizedBox(height: 4),
// Ligne 2 : Adresse courte + Badge montant à droite
// Ligne 2 : Adresse courte + Icônes + Badge montant à droite
Row(
children: [
// Adresse courte
@@ -359,6 +403,76 @@ class PassagesListWidget extends StatelessWidget {
),
const SizedBox(width: 8),
// Icône remarque (si présente)
if (passage['remarque'] != null &&
(passage['remarque'] as String).trim().isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Tooltip(
message: passage['remarque'],
preferBelow: false,
child: Icon(
Icons.comment_outlined,
size: 16,
color: theme.colorScheme.primary.withOpacity(0.7),
),
),
),
// Icône email (si présent)
if (passage['email'] != null &&
(passage['email'] as String).trim().isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 8),
child: InkWell(
onTap: () {
final email = passage['email'] as String;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Email: $email'),
duration: const Duration(seconds: 3),
action: SnackBarAction(
label: 'OK',
onPressed: () {},
),
),
);
},
borderRadius: BorderRadius.circular(4),
child: Tooltip(
message: passage['email'],
preferBelow: false,
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
Icons.alternate_email,
size: 16,
color: (passage['emailErreur'] != null &&
(passage['emailErreur'] as String).trim().isNotEmpty)
? Colors.red.withOpacity(0.7)
: Colors.blue.withOpacity(0.7),
),
),
),
),
),
// Icône reçu (si présent)
if (passage['nomRecu'] != null &&
(passage['nomRecu'] as String).trim().isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Tooltip(
message: 'Reçu disponible',
preferBelow: false,
child: Icon(
Icons.receipt_outlined,
size: 16,
color: theme.colorScheme.secondary.withOpacity(0.7),
),
),
),
// Badge montant (si > 0 et type 1 ou 5)
if (isPaid && (typeId == 1 || typeId == 5))
Builder(
@@ -368,17 +482,18 @@ class PassagesListWidget extends StatelessWidget {
passage['payment'] as int? ??
4; // 4 = Non renseigné par défaut
// Récupérer l'icône du type de règlement
// Récupérer l'icône ET la couleur du type de règlement
final reglementInfo = AppKeys.typesReglements[typeReglement];
final reglementIcon = reglementInfo?['icon_data'] as IconData? ?? Icons.help_outline;
final reglementColor = Color(reglementInfo?['couleur'] as int? ?? 0xFF9E9E9E); // Gris par défaut
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.15),
color: reglementColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.green.withValues(alpha: 0.4),
color: reglementColor.withOpacity(0.4),
),
),
child: Row(
@@ -387,13 +502,13 @@ class PassagesListWidget extends StatelessWidget {
Icon(
reglementIcon,
size: 12,
color: Colors.green.shade700,
color: reglementColor,
),
const SizedBox(width: 4),
Text(
formattedAmount,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.green.shade700,
color: reglementColor,
fontWeight: FontWeight.bold,
fontSize: 11,
),
@@ -403,6 +518,29 @@ class PassagesListWidget extends StatelessWidget {
);
},
),
// Icône QR Code (si Payment Link généré)
if (passage['stripe_payment_link_id'] != null &&
(passage['stripe_payment_link_id'] as String).trim().isNotEmpty)
Padding(
padding: const EdgeInsets.only(left: 4),
child: InkWell(
onTap: () => _showQRCodeDialog(context, passage),
borderRadius: BorderRadius.circular(4),
child: Tooltip(
message: 'Afficher le QR Code',
preferBelow: false,
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
Icons.qr_code_2,
size: 16,
color: Colors.blue.withOpacity(0.7),
),
),
),
),
),
],
),
],
@@ -414,4 +552,71 @@ class PassagesListWidget extends StatelessWidget {
),
);
}
/// Construit le texte du nombre de passages avec le type si filtré
String _buildPassageCountText() {
final count = passages.length;
final baseText = '$count passage${count > 1 ? 's' : ''}';
// Si un type de passage est filtré et différent de "Tous les types"
if (filteredPassageType != null && filteredPassageType!.isNotEmpty) {
final typeLowerCase = filteredPassageType!.toLowerCase();
// Gérer le pluriel selon le type
String typeWithPlural;
if (count > 1) {
// Gestion des pluriels spécifiques
if (typeLowerCase == 'à finaliser') {
typeWithPlural = 'à finaliser'; // Invariable
} else if (typeLowerCase.endsWith('é')) {
typeWithPlural = '${typeLowerCase}s'; // effectué → effectués, refusé → refusés
} else if (typeLowerCase == 'maison vide') {
typeWithPlural = 'maisons vides';
} else {
typeWithPlural = '${typeLowerCase}s'; // don → dons, lot → lots
}
} else {
typeWithPlural = typeLowerCase;
}
return '$count passage${count > 1 ? 's' : ''} $typeWithPlural';
}
return baseText;
}
/// Afficher le QR Code pour un passage avec Payment Link
void _showQRCodeDialog(BuildContext context, Map<String, dynamic> passage) {
final paymentLinkUrl = passage['stripe_payment_link_url'] as String?;
final paymentLinkId = passage['stripe_payment_link_id'] as String?;
if (paymentLinkUrl == null || paymentLinkUrl.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('URL du QR Code non disponible'),
duration: Duration(seconds: 2),
),
);
return;
}
// Récupérer le montant du passage
final montantStr = passage['montant'] as String? ?? '0';
final montant = double.tryParse(montantStr.replaceAll(',', '.')) ?? 0;
final amountInCents = (montant * 100).round();
// Créer un PaymentLinkResult avec les données du passage
final paymentLink = PaymentLinkResult(
paymentLinkId: paymentLinkId ?? '',
url: paymentLinkUrl,
amount: amountInCents,
passageId: passage['id'] as int?,
);
// Afficher le QR Code
QRCodePaymentDialog.show(
context: context,
paymentLink: paymentLink,
);
}
}