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
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('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 'rejetee':
|
|
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'] ?? '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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|