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:
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user