You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1121 lines
44 KiB

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class AjoutPointDeVentePage extends StatefulWidget {
const AjoutPointDeVentePage({super.key});
@override
_AjoutPointDeVentePageState createState() => _AjoutPointDeVentePageState();
}
class _AjoutPointDeVentePageState extends State<AjoutPointDeVentePage> {
final AppDatabase _appDatabase = AppDatabase.instance;
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
// Contrôleurs
final TextEditingController _nomController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
// Liste des points de vente
List<Map<String, dynamic>> _pointsDeVente = [];
List<Map<String, dynamic>> _filteredPointsDeVente = [];
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadPointsDeVente();
_searchController.addListener(_filterPointsDeVente);
}
Future<void> _loadPointsDeVente() async {
if (mounted) {
setState(() {
_isLoading = true;
});
}
try {
final points = await _appDatabase.getPointsDeVente();
// Enrichir chaque point de vente avec les informations de contraintes
for (var point in points) {
final verification = await _appDatabase.checkCanDeletePointDeVente(point['id']);
point['canDelete'] = verification['canDelete'];
point['constraintCount'] = (verification['reasons'] as List).length;
}
if (mounted) {
setState(() {
_pointsDeVente = points;
_filteredPointsDeVente = List.from(points);
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Erreur',
'Impossible de charger les points de vente: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
}
void _filterPointsDeVente() {
final query = _searchController.text.toLowerCase().trim();
if (mounted) {
setState(() {
if (query.isEmpty) {
_filteredPointsDeVente = List.from(_pointsDeVente);
} else {
_filteredPointsDeVente = _pointsDeVente.where((point) {
final nom = point['nom']?.toString().toLowerCase() ?? '';
final code = point['code']?.toString().toLowerCase() ?? '';
return nom.contains(query) || code.contains(query);
}).toList();
}
});
}
}
Future<void> _submitForm() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
await _appDatabase.createPointDeVente(
_nomController.text.trim(),
_codeController.text.trim(),
);
// Réinitialiser le formulaire
_nomController.clear();
_codeController.clear();
// Recharger la liste
await _loadPointsDeVente();
Get.snackbar(
'Succès',
'Point de vente ajouté avec succès',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible d\'ajouter le point de vente: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
Future<void> editPointDeVente(BuildContext context, Map<String, dynamic> pointDeVente) async {
print('=== DIAGNOSTIC DES DONNÉES ===');
print('ID: ${pointDeVente['id']}');
print('Nom: ${pointDeVente['nom']}');
print('Code: ${pointDeVente['code']}');
print('Content (ticket): "${pointDeVente['content']}" (Type: ${pointDeVente['content'].runtimeType})');
print('Livraison: "${pointDeVente['livraison']}" (Type: ${pointDeVente['livraison'].runtimeType})');
print('Facture: "${pointDeVente['facture']}" (Type: ${pointDeVente['facture'].runtimeType})');
print('Logo: ${pointDeVente['logo']?.runtimeType}');
print('Toutes les clés: ${pointDeVente.keys.toList()}');
print('===============================');
final _editFormKey = GlobalKey<FormState>();
final ImagePicker _picker = ImagePicker();
// Fonction helper pour convertir les valeurs en String de manière sûre
String safeStringConversion(dynamic value) {
if (value == null) return '';
if (value is String) return value;
// Vérifier si c'est un type binaire (Uint8List, List<int>, etc.)
if (value is Uint8List || value is List<int> || value is List) {
// Si c'est des données binaires, on retourne une chaîne vide
// car on ne peut pas les convertir en texte lisible
return '';
}
// Pour tous les autres types, essayer la conversion toString()
try {
return value.toString();
} catch (e) {
print('Erreur conversion vers String: $e, type: ${value.runtimeType}');
return '';
}
}
// Initialisation dynamique des contrôleurs avec conversion sécurisée
final _editNomController = TextEditingController(
text: safeStringConversion(pointDeVente['nom'])
);
final _editCodeController = TextEditingController(
text: safeStringConversion(pointDeVente['code'])
);
final _editTicketController = TextEditingController(
text: safeStringConversion(pointDeVente['content'])
);
final _editLivraisonController = TextEditingController(
text: safeStringConversion(pointDeVente['livraison'])
);
final _editFactureController = TextEditingController(
text: safeStringConversion(pointDeVente['facture'])
);
File? _selectedImage;
Uint8List? _currentImageBlob;
// Gérer la conversion du logo de manière sécurisée
final logoData = pointDeVente['logo'];
if (logoData != null) {
try {
if (logoData is Uint8List) {
_currentImageBlob = logoData;
} else if (logoData is List<int>) {
_currentImageBlob = Uint8List.fromList(logoData);
} else if (logoData is List) {
// Cas où c'est une List<dynamic> contenant des int
_currentImageBlob = Uint8List.fromList(logoData.cast<int>());
} else {
// Type non supporté (comme Blob), laisser null et logger
print('Type de logo non supporté: ${logoData.runtimeType}');
_currentImageBlob = null;
}
} catch (e) {
print('Erreur lors de la conversion du logo: $e');
_currentImageBlob = null;
}
}
bool _isEditLoading = false;
Future<void> pickImage(ImageSource source) async {
try {
final XFile? image = await _picker.pickImage(source: source);
if (image != null) {
_selectedImage = File(image.path);
_currentImageBlob = await _selectedImage!.readAsBytes();
}
} catch (e) {
print('Erreur lors de la sélection de l\'image : $e');
}
}
Future<void> saveChanges() async {
if (_editFormKey.currentState?.validate() ?? false) {
_isEditLoading = true;
try {
await _appDatabase.updatePointDeVentes(
pointDeVente['id'],
_editNomController.text.trim(),
_editCodeController.text.trim(),
content: _editTicketController.text.trim(),
livraison: _editLivraisonController.text.trim(),
facture: _editFactureController.text.trim(),
imagePath: _currentImageBlob,
);
Get.back(); // Fermer le dialog
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Modification enregistrée avec succès')),
);
await _loadPointsDeVente(); // Rafraîchir la liste
} catch (e) {
print('Erreur lors de la sauvegarde : $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Erreur lors de la modification')),
);
} finally {
_isEditLoading = false;
}
}
}
// Affichage de la boîte de dialogue
await Get.dialog(
Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 700),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _editFormKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Modifier le point de vente', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
TextFormField(
controller: _editNomController,
decoration: const InputDecoration(labelText: 'Nom du point de vente'),
validator: (value) => value == null || value.isEmpty ? 'Le nom est requis' : null,
),
const SizedBox(height: 8),
TextFormField(
controller: _editCodeController,
decoration: const InputDecoration(labelText: 'Code du point de vente'),
),
const SizedBox(height: 8),
TextFormField(
controller: _editTicketController,
decoration: const InputDecoration(labelText: 'Info ticket'),
maxLines: 3,
),
const SizedBox(height: 8),
TextFormField(
controller: _editLivraisonController,
decoration: const InputDecoration(labelText: 'Info bon de livraison'),
maxLines: 3,
),
const SizedBox(height: 8),
TextFormField(
controller: _editFactureController,
decoration: const InputDecoration(labelText: 'Info facture'),
maxLines: 3,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => pickImage(ImageSource.gallery),
icon: const Icon(Icons.image),
label: const Text('Galerie'),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton.icon(
onPressed: () => pickImage(ImageSource.camera),
icon: const Icon(Icons.camera_alt),
label: const Text('Caméra'),
),
),
],
),
const SizedBox(height: 10),
if (_currentImageBlob != null)
Container(
height: 100,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(
_currentImageBlob!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Center(child: Text('Erreur image')),
),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isEditLoading ? null : saveChanges,
icon: const Icon(Icons.save),
label: Text(_isEditLoading ? 'Sauvegarde...' : 'Enregistrer'),
),
),
],
),
),
),
),
),
),
);
// Nettoyage
await Future.delayed(const Duration(milliseconds: 200));
_editNomController.dispose();
_editCodeController.dispose();
_editTicketController.dispose();
_editLivraisonController.dispose();
_editFactureController.dispose();
}
Future<void> _showConstraintDialog(int id, Map<String, dynamic> verificationResult) async {
final reasons = verificationResult['reasons'] as List<String>;
final suggestions = verificationResult['suggestions'] as List<String>;
await Get.dialog(
AlertDialog(
title: const Row(
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Expanded(child: Text('Suppression impossible')),
],
),
content: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Ce point de vente ne peut pas être supprimé pour les raisons suivantes :',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
...reasons.map((reason) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('', style: TextStyle(color: Colors.red)),
Expanded(child: Text(reason)),
],
),
)),
if (suggestions.isNotEmpty) ...[
const SizedBox(height: 16),
const Text(
'Solutions possibles :',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue),
),
const SizedBox(height: 8),
...suggestions.map((suggestion) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('💡 ', style: TextStyle(fontSize: 12)),
Expanded(child: Text(suggestion, style: const TextStyle(fontSize: 13))),
],
),
)),
],
],
),
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Fermer'),
),
if (reasons.any((r) => r.contains('produit')))
ElevatedButton(
onPressed: () {
Get.back();
_showTransferDialog(id);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: const Text('Transférer'),
),
],
),
);
}
Future<void> _showTransferDialog(int sourcePointDeVenteId) async {
final pointsDeVente = await _appDatabase.getPointsDeVenteForTransfer(sourcePointDeVenteId);
if (pointsDeVente.isEmpty) {
Get.snackbar(
'Erreur',
'Aucun autre point de vente disponible pour le transfert',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
int? selectedPointDeVenteId;
await Get.dialog(
AlertDialog(
title: const Text('Transférer les produits'),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 300),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Sélectionnez le point de vente de destination pour les produits :'),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: selectedPointDeVenteId,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Point de vente de destination',
border: OutlineInputBorder(),
),
items: pointsDeVente.map((pv) => DropdownMenuItem<int>(
value: pv['id'] as int,
child: Text(
pv['nom'] as String,
overflow: TextOverflow.ellipsis,
),
)).toList(),
onChanged: (value) {
selectedPointDeVenteId = value;
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () async {
if (selectedPointDeVenteId != null) {
Get.back();
await _performTransferAndDelete(sourcePointDeVenteId, selectedPointDeVenteId!);
} else {
Get.snackbar(
'Erreur',
'Veuillez sélectionner un point de vente de destination',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
child: const Text('Transférer'),
),
],
),
);
}
Future<void> _performTransferAndDelete(int sourceId, int targetId) async {
if (mounted) {
setState(() {
_isLoading = true;
});
}
try {
final confirmed = await Get.dialog<bool>(
AlertDialog(
title: const Text('Confirmation finale'),
content: const Text(
'Cette action va transférer tous les produits vers le point de vente sélectionné '
'puis supprimer définitivement le point de vente original. '
'Cette action est irréversible. Continuer ?'
),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Get.back(result: true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Confirmer'),
),
],
),
);
if (confirmed == true) {
await _appDatabase.deletePointDeVenteWithTransfer(sourceId, targetId);
await _loadPointsDeVente();
Get.snackbar(
'Succès',
'Produits transférés et point de vente supprimé avec succès',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
}
} catch (e) {
Get.snackbar(
'Erreur',
'Erreur lors du transfert: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _showPointDeVenteDetails(Map<String, dynamic> pointDeVente) async {
final id = pointDeVente['id'] as int;
try {
final stats = await _getPointDeVenteStats(id);
await Get.dialog(
AlertDialog(
title: Text(
'Détails: ${pointDeVente['nom']}',
style: const TextStyle(fontSize: 16),
),
content: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
maxHeight: 500,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (pointDeVente['logo'] != null)
Container(
height: 150,
width: double.infinity,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(
pointDeVente['logo'] as Uint8List,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey.shade200,
child: const Center(
child: Text('Image non disponible'),
),
);
},
),
),
),
_buildStatRow('Produits associés', '${stats['produits']}'),
_buildStatRow('Utilisateurs associés', '${stats['utilisateurs']}'),
_buildStatRow('Demandes de transfert', '${stats['transferts']}'),
const SizedBox(height: 8),
if (pointDeVente['content'] != null &&
pointDeVente['content'].toString().isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'Ticket: ${pointDeVente['content']}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
),
if (pointDeVente['livraison'] != null &&
pointDeVente['livraison'].toString().isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'Bon de livraison: ${pointDeVente['livraison']}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
),
if (pointDeVente['facture'] != null &&
pointDeVente['facture'].toString().isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'Facture: ${pointDeVente['facture']}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Fermer'),
),
],
),
);
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de récupérer les détails: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
Widget _buildStatRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
label,
style: const TextStyle(fontSize: 13),
),
),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
],
),
);
}
Future<Map<String, int>> _getPointDeVenteStats(int id) async {
final verification = await _appDatabase.checkCanDeletePointDeVente(id);
// Parser les raisons pour extraire les nombres
int produits = 0, utilisateurs = 0, transferts = 0;
for (String reason in verification['reasons']) {
if (reason.contains('produit')) {
produits = int.tryParse(reason.split(' ')[0]) ?? 0;
} else if (reason.contains('utilisateur')) {
utilisateurs = int.tryParse(reason.split(' ')[0]) ?? 0;
} else if (reason.contains('transfert')) {
transferts = int.tryParse(reason.split(' ')[0]) ?? 0;
}
}
return {
'produits': produits,
'utilisateurs': utilisateurs,
'transferts': transferts,
};
}
Future<void> _deletePointDeVente(int id) async {
final verificationResult = await _appDatabase.checkCanDeletePointDeVente(id);
if (!verificationResult['canDelete']) {
await _showConstraintDialog(id, verificationResult);
return;
}
final confirmed = await Get.dialog<bool>(
AlertDialog(
title: const Text('Confirmer la suppression'),
content: const Text('Voulez-vous vraiment supprimer ce point de vente ?'),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Get.back(result: true),
child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true) {
if (mounted) {
setState(() {
_isLoading = true;
});
}
try {
await _appDatabase.deletePointDeVente(id);
await _loadPointsDeVente();
Get.snackbar(
'Succès',
'Point de vente supprimé avec succès',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de supprimer le point de vente: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'Gestion des points de vente'),
drawer: CustomDrawer(),
body: Column(
children: [
// Formulaire d'ajout
Card(
margin: const EdgeInsets.all(16),
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Ajouter un point de vente',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _nomController,
decoration: InputDecoration(
labelText: 'Nom du point de vente',
prefixIcon: const Icon(Icons.store),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un nom';
}
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _codeController,
decoration: InputDecoration(
labelText: 'Code (optionnel)',
prefixIcon: const Icon(Icons.code),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Ajouter le point de vente',
style: TextStyle(fontSize: 16),
),
),
],
),
),
),
),
// Liste des points de vente
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
// Barre de recherche
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Rechercher un point de vente',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_filterPointsDeVente();
},
)
: null,
),
),
),
// Liste
Expanded(
child: _isLoading && _filteredPointsDeVente.isEmpty
? const Center(child: CircularProgressIndicator())
: _filteredPointsDeVente.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.store_mall_directory_outlined,
size: 60, color: Colors.grey),
SizedBox(height: 16),
Text(
'Aucun point de vente trouvé',
style: TextStyle(
fontSize: 16,
color: Colors.grey),
),
],
),
)
: ListView.builder(
itemCount: _filteredPointsDeVente.length,
itemBuilder: (context, index) {
final point = _filteredPointsDeVente[index];
final canDelete = point['canDelete'] ?? true;
final constraintCount = point['constraintCount'] ?? 0;
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: !canDelete ? Border.all(
color: Colors.orange.shade300,
width: 1,
) : null,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
// Icône avec indicateur de statut
Stack(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: canDelete ? Colors.blue.shade50 : Colors.orange.shade50,
borderRadius: BorderRadius.circular(6),
),
child: Icon(
Icons.store,
color: canDelete ? Colors.blue.shade700 : Colors.orange.shade700,
size: 20,
),
),
if (!canDelete)
Positioned(
right: 0,
top: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
child: Text(
'$constraintCount',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
const SizedBox(width: 12),
// Informations du point de vente
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
point['nom'] ?? 'N/A',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
),
),
),
if (!canDelete)
Icon(
Icons.link,
size: 16,
color: Colors.orange.shade600,
),
],
),
Row(
children: [
if (point['code'] != null && point['code'].toString().isNotEmpty)
Text(
'Code: ${point['code']}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
if (!canDelete) ...[
const SizedBox(width: 8),
Text(
'$constraintCount contrainte(s)',
style: TextStyle(
color: Colors.orange.shade600,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
],
),
],
),
),
// Boutons d'actions
Row(
mainAxisSize: MainAxisSize.min,
children: [
// Bouton détails
IconButton(
icon: Icon(
Icons.info_outline,
size: 20,
color: Colors.blue.shade600,
),
onPressed: () => _showPointDeVenteDetails(point),
tooltip: 'Voir les détails',
),
// Bouton modifier
IconButton(
icon: Icon(
Icons.edit,
size: 20,
color: Colors.blue.shade600,
),
onPressed: () => editPointDeVente(context, point),
tooltip: 'Modifier point de vente',
),
// Bouton suppression avec indication visuelle
IconButton(
icon: Icon(
canDelete ? Icons.delete_outline : Icons.delete_forever_outlined,
size: 20,
color: canDelete ? Colors.red : Colors.orange.shade700,
),
onPressed: () => _deletePointDeVente(point['id']),
tooltip: canDelete ? 'Supprimer' : 'Supprimer (avec contraintes)',
),
],
),
],
),
),
),
);
},
),
),
],
),
),
),
],
),
);
}
@override
void dispose() {
_nomController.dispose();
_codeController.dispose();
_searchController.dispose();
super.dispose();
}
}