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.
 
 
 
 
 
 

847 lines
28 KiB

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<GestionTransfertsPage> with TickerProviderStateMixin {
final AppDatabase _appDatabase = AppDatabase.instance;
final UserController _userController = Get.find<UserController>();
List<Map<String, dynamic>> _demandes = [];
List<Map<String, dynamic>> _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<void> _loadDemandes() async {
setState(() => _isLoading = true);
try {
List<Map<String, dynamic>> 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<void> _validerDemande(int demandeId, Map<String, dynamic> 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<void> _rejeterDemande(int demandeId, Map<String, dynamic> 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<bool> _showConfirmationDialog(Map<String, dynamic> 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<bool>(
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<void> _showRuptureStockDialog(Map<String, dynamic> 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<String?> _showRejectionDialog() async {
final TextEditingController motifController = TextEditingController();
return await showDialog<String>(
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<String, dynamic> 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,
),
),
),
],
),
],
],
),
),
);
}
}