import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/controller/userController.dart'; class GestionTransfertsPage extends StatefulWidget { const GestionTransfertsPage({super.key}); @override _GestionTransfertsPageState createState() => _GestionTransfertsPageState(); } class _GestionTransfertsPageState extends State with TickerProviderStateMixin { final AppDatabase _appDatabase = AppDatabase.instance; final UserController _userController = Get.find(); List> _demandes = []; List> _filteredDemandes = []; bool _isLoading = false; String _selectedStatut = 'en_attente'; String _searchQuery = ''; late TabController _tabController; final TextEditingController _searchController = TextEditingController(); @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); _loadDemandes(); _searchController.addListener(_filterDemandes); } @override void dispose() { _tabController.dispose(); _searchController.dispose(); super.dispose(); } Future _loadDemandes() async { setState(() => _isLoading = true); try { List> demandes; switch (_selectedStatut) { case 'en_attente': demandes = await _appDatabase.getDemandesTransfertEnAttente(); break; case 'validees': demandes = await _appDatabase.getDemandesTransfertValidees(); break; case 'toutes': demandes = await _appDatabase.getToutesDemandesTransfert(); break; default: demandes = await _appDatabase.getDemandesTransfertEnAttente(); } setState(() { _demandes = demandes; _filteredDemandes = demandes; }); _filterDemandes(); } catch (e) { Get.snackbar( 'Erreur', 'Impossible de charger les demandes: $e', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); } finally { setState(() => _isLoading = false); } } void _filterDemandes() { final query = _searchController.text.toLowerCase(); setState(() { _filteredDemandes = _demandes.where((demande) { final produitNom = (demande['produit_nom'] ?? '').toString().toLowerCase(); final produitRef = (demande['produit_reference'] ?? '').toString().toLowerCase(); final demandeurNom = (demande['demandeur_nom'] ?? '').toString().toLowerCase(); final pointVenteSource = (demande['point_vente_source'] ?? '').toString().toLowerCase(); final pointVenteDestination = (demande['point_vente_destination'] ?? '').toString().toLowerCase(); return produitNom.contains(query) || produitRef.contains(query) || demandeurNom.contains(query) || pointVenteSource.contains(query) || pointVenteDestination.contains(query); }).toList(); }); } Future _validerDemande(int demandeId, Map demande) async { // Vérifier seulement si le produit est en rupture de stock (stock = 0) final stockDisponible = demande['stock_source'] as int? ?? 0; if (stockDisponible == 0) { await _showRuptureStockDialog(demande); return; } final confirmation = await _showConfirmationDialog(demande); if (!confirmation) return; setState(() => _isLoading = true); try { await _appDatabase.validerTransfert(demandeId, _userController.userId); Get.snackbar( 'Succès', 'Transfert validé avec succès', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 3), icon: const Icon(Icons.check_circle, color: Colors.white), ); await _loadDemandes(); } catch (e) { Get.snackbar( 'Erreur', 'Impossible de valider le transfert: $e', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 4), icon: const Icon(Icons.error, color: Colors.white), ); } finally { setState(() => _isLoading = false); } } Future _rejeterDemande(int demandeId, Map demande) async { final motif = await _showRejectionDialog(); if (motif == null) return; setState(() => _isLoading = true); try { await _appDatabase.rejeterTransfert(demandeId, _userController.userId, motif); Get.snackbar( 'Demande rejetée', 'La demande de transfert a été rejetée', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange, colorText: Colors.white, duration: const Duration(seconds: 3), ); await _loadDemandes(); } catch (e) { Get.snackbar( 'Erreur', 'Impossible de rejeter la demande: $e', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); } finally { setState(() => _isLoading = false); } } Future _showConfirmationDialog(Map demande) async { final stockDisponible = demande['stock_source'] as int? ?? 0; final quantiteDemandee = demande['quantite'] as int; final stockInsuffisant = stockDisponible < quantiteDemandee && stockDisponible > 0; return await showDialog( context: context, builder: (context) => AlertDialog( title: Row( children: [ Icon(Icons.swap_horiz, color: Colors.blue.shade700), const SizedBox(width: 8), const Text('Confirmer le transfert'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Êtes-vous sûr de vouloir valider ce transfert ?'), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Produit: ${demande['produit_nom']}', style: const TextStyle(fontWeight: FontWeight.bold), ), Text('Référence: ${demande['produit_reference']}'), Text('Quantité: ${demande['quantite']}'), Text(demande['point_vente_source'] == demande['point_vente_destination']?'De: Non specifier' : 'De: ${demande['point_vente_source']}'), Text('Vers: ${demande['point_vente_destination']}'), Text( 'Stock disponible: $stockDisponible', style: TextStyle( color: stockInsuffisant ? Colors.orange.shade700 : Colors.green.shade700, fontWeight: FontWeight.w500, ), ), ], ), ), if (stockInsuffisant) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange.shade200), ), child: Row( children: [ Icon(Icons.info, size: 16, color: Colors.orange.shade600), const SizedBox(width: 8), Expanded( child: Text( 'Le stock sera insuffisant après ce transfert', style: TextStyle( fontSize: 12, color: Colors.orange.shade700, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, ), child: const Text('Valider'), ), ], ), ) ?? false; } Future _showRuptureStockDialog(Map demande) async { await showDialog( context: context, builder: (context) => AlertDialog( title: Row( children: [ Icon(Icons.warning, color: Colors.red.shade700), const SizedBox(width: 8), const Text('Rupture de stock'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Impossible d\'effectuer ce transfert car le produit est en rupture de stock.', style: TextStyle(color: Colors.red.shade700), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Produit: ${demande['produit_nom']}'), Text('Quantité demandée: ${demande['quantite']}'), Text( 'Stock disponible: 0', style: TextStyle( color: Colors.red.shade700, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), actions: [ ElevatedButton( onPressed: () => Navigator.pop(context), child: const Text('Compris'), ), ], ), ); } Future _showRejectionDialog() async { final TextEditingController motifController = TextEditingController(); return await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Rejeter la demande'), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('Veuillez indiquer le motif du rejet :'), const SizedBox(height: 12), TextField( controller: motifController, decoration: const InputDecoration( hintText: 'Motif du rejet', border: OutlineInputBorder(), ), maxLines: 3, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler'), ), ElevatedButton( onPressed: () { if (motifController.text.trim().isNotEmpty) { Navigator.pop(context, motifController.text.trim()); } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, foregroundColor: Colors.white, ), child: const Text('Rejeter'), ), ], ), ); } @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width < 600; return Scaffold( appBar: AppBar( title: const Text('Gestion des transferts'), backgroundColor: Colors.blue.shade700, foregroundColor: Colors.white, elevation: 0, actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _loadDemandes, tooltip: 'Actualiser', ), ], bottom: TabBar( controller: _tabController, onTap: (index) { setState(() { switch (index) { case 0: _selectedStatut = 'en_attente'; break; case 1: _selectedStatut = 'validees'; break; case 2: _selectedStatut = 'toutes'; break; } }); _loadDemandes(); }, labelColor: Colors.white, unselectedLabelColor: Colors.white70, indicatorColor: Colors.white, tabs: const [ Tab( icon: Icon(Icons.pending_actions), text: 'En attente', ), Tab( icon: Icon(Icons.check_circle), text: 'Validées', ), Tab( icon: Icon(Icons.list), text: 'Toutes', ), ], ), ), drawer: CustomDrawer(), body: Column( children: [ // Barre de recherche Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher par produit, référence, demandeur...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); _filterDemandes(); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), filled: true, fillColor: Colors.grey.shade100, ), ), ), // Compteur de résultats Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Text( '${_filteredDemandes.length} demande(s)', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.grey.shade700, ), ), const Spacer(), if (_selectedStatut == 'en_attente' && _filteredDemandes.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(12), ), child: Text( 'Action requise', style: TextStyle( fontSize: 12, color: Colors.orange.shade700, fontWeight: FontWeight.bold, ), ), ), ], ), ), // Liste des demandes Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : _filteredDemandes.isEmpty ? _buildEmptyState() : TabBarView( controller: _tabController, children: [ _buildDemandesEnAttente(), _buildDemandesValidees(), _buildToutesLesDemandes(), ], ), ), ], ), ); } Widget _buildEmptyState() { String message; IconData icon; switch (_selectedStatut) { case 'en_attente': message = 'Aucune demande en attente'; icon = Icons.inbox; break; case 'validees': message = 'Aucune demande validée'; icon = Icons.check_circle_outline; break; default: message = 'Aucune demande trouvée'; icon = Icons.search_off; } return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, size: 64, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( message, style: TextStyle( fontSize: 18, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), ), if (_searchController.text.isNotEmpty) ...[ const SizedBox(height: 8), Text( 'Aucun résultat pour "${_searchController.text}"', style: TextStyle( fontSize: 14, color: Colors.grey.shade500, ), ), ], ], ), ); } Widget _buildDemandesEnAttente() { return _buildDemandesList(showActions: true); } Widget _buildDemandesValidees() { return _buildDemandesList(showActions: false); } Widget _buildToutesLesDemandes() { return _buildDemandesList(showActions: _selectedStatut == 'en_attente'); } Widget _buildDemandesList({required bool showActions}) { return RefreshIndicator( onRefresh: _loadDemandes, child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _filteredDemandes.length, itemBuilder: (context, index) { final demande = _filteredDemandes[index]; return _buildDemandeCard(demande, showActions); }, ), ); } Widget _buildDemandeCard(Map demande, bool showActions) { final isMobile = MediaQuery.of(context).size.width < 600; final statut = demande['statut'] as String? ?? 'en_attente'; final stockDisponible = demande['stock_source'] as int? ?? 0; final quantiteDemandee = demande['quantite'] as int; final enRuptureStock = stockDisponible == 0; Color statutColor; IconData statutIcon; String statutText; switch (statut) { case 'validee': statutColor = Colors.green; statutIcon = Icons.check_circle; statutText = 'Validée'; break; case 'refusee': statutColor = Colors.red; statutIcon = Icons.cancel; statutText = 'Rejetée'; break; default: statutColor = Colors.orange; statutIcon = Icons.pending; statutText = 'En attente'; } return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: enRuptureStock && statut == 'en_attente' ? BorderSide(color: Colors.red.shade300, width: 1.5) : BorderSide.none, ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête avec produit et statut Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( demande['produit_nom'] ?? 'Produit inconnu', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 4), Text( 'Réf: ${demande['produit_reference'] ?? 'N/A'}', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: statutColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: statutColor.withOpacity(0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(statutIcon, size: 16, color: statutColor), const SizedBox(width: 4), Text( statutText, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: statutColor, ), ), ], ), ), ], ), const SizedBox(height: 12), // Informations de transfert Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Row( children: [ Icon(Icons.store, size: 16, color: Colors.blue.shade600), const SizedBox(width: 8), Expanded( child: Text( demande['point_vente_source']==demande['point_vente_destination']?"Non specifier" : '${demande['point_vente_source'] ?? 'N/A'}', style: const TextStyle(fontWeight: FontWeight.w500), ), ), Icon(Icons.arrow_forward, size: 16, color: Colors.grey.shade600), const SizedBox(width: 8), Expanded( child: Text( '${demande['point_vente_destination'] ?? 'N/A'}', style: const TextStyle(fontWeight: FontWeight.w500), textAlign: TextAlign.end, ), ), ], ), const SizedBox(height: 8), Row( children: [ Icon(Icons.inventory_2, size: 16, color: Colors.green.shade600), const SizedBox(width: 8), Text('Quantité: $quantiteDemandee'), const Spacer(), Text( 'Stock source: $stockDisponible', style: TextStyle( color: enRuptureStock ? Colors.red.shade600 : stockDisponible < quantiteDemandee ? Colors.orange.shade600 : Colors.green.shade600, fontWeight: FontWeight.w500, ), ), ], ), ], ), ), const SizedBox(height: 12), // Informations de la demande Row( children: [ Icon(Icons.person, size: 16, color: Colors.grey.shade600), const SizedBox(width: 8), Expanded( child: Text( 'Demandé par: ${demande['demandeur_nom'] ?? 'N/A'}', style: TextStyle( fontSize: 14, color: Colors.grey.shade700, ), ), ), ], ), const SizedBox(height: 4), Row( children: [ Icon(Icons.access_time, size: 16, color: Colors.grey.shade600), const SizedBox(width: 8), Text( DateFormat('dd/MM/yyyy à HH:mm').format( (demande['date_demande'] as DateTime).toLocal() ), style: TextStyle( fontSize: 14, color: Colors.grey.shade700, ), ), ], ), // Alerte rupture de stock if (enRuptureStock && statut == 'en_attente') ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200), ), child: Row( children: [ Icon(Icons.warning, size: 16, color: Colors.red.shade600), const SizedBox(width: 8), Expanded( child: Text( 'Produit en rupture de stock', style: TextStyle( fontSize: 12, color: Colors.red.shade700, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], // Actions (seulement pour les demandes en attente) if (showActions && statut == 'en_attente') ...[ const SizedBox(height: 16), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => _rejeterDemande( demande['id'] as int, demande, ), icon: const Icon(Icons.close, size: 18), label: Text(isMobile ? 'Rejeter' : 'Rejeter'), style: OutlinedButton.styleFrom( foregroundColor: Colors.red.shade600, side: BorderSide(color: Colors.red.shade300), ), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: !enRuptureStock ? () => _validerDemande( demande['id'] as int, demande, ) : null, icon: const Icon(Icons.check, size: 18), label: Text(isMobile ? 'Valider' : 'Valider'), style: ElevatedButton.styleFrom( backgroundColor: !enRuptureStock ? Colors.green.shade600 : Colors.grey.shade400, foregroundColor: Colors.white, ), ), ), ], ), ], ], ), ), ); } }