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.
 
 
 
 
 
 

452 lines
15 KiB

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
class ApprobationSortiesPage extends StatefulWidget {
const ApprobationSortiesPage({super.key});
@override
_ApprobationSortiesPageState createState() => _ApprobationSortiesPageState();
}
class _ApprobationSortiesPageState extends State<ApprobationSortiesPage> {
final AppDatabase _database = AppDatabase.instance;
final UserController _userController = Get.find<UserController>();
List<Map<String, dynamic>> _sortiesEnAttente = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadSortiesEnAttente();
}
Future<void> _loadSortiesEnAttente() async {
setState(() => _isLoading = true);
try {
final sorties = await _database.getSortiesPersonnellesEnAttente();
setState(() {
_sortiesEnAttente = sorties;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
Get.snackbar('Erreur', 'Impossible de charger les demandes: $e');
}
}
Future<void> _approuverSortie(Map<String, dynamic> sortie) async {
final confirm = await Get.dialog<bool>(
AlertDialog(
title: const Text('Approuver la demande'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Produit: ${sortie['produit_nom']}'),
Text('Quantité: ${sortie['quantite']}'),
Text('Demandeur: ${sortie['admin_nom']}'),
Text('Motif: ${sortie['motif']}'),
Text('Note: ${sortie['notes']}'),
const SizedBox(height: 16),
const Text(
'Confirmer l\'approbation de cette demande de sortie personnelle ?',
style: TextStyle(fontWeight: FontWeight.w600),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Get.back(result: true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
child: const Text('Approuver', style: TextStyle(color: Colors.white)),
),
],
),
);
if (confirm == true) {
try {
await _database.approuverSortiePersonnelle(
sortie['id'] as int,
_userController.userId,
);
Get.snackbar(
'Demande approuvée',
'La sortie personnelle a été approuvée et le stock mis à jour',
backgroundColor: Colors.green,
colorText: Colors.white,
);
_loadSortiesEnAttente();
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible d\'approuver la demande: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
}
Future<void> _refuserSortie(Map<String, dynamic> sortie) async {
final motifController = TextEditingController();
final confirm = await Get.dialog<bool>(
AlertDialog(
title: const Text('Refuser la demande'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Demande de: ${sortie['admin_nom']}'),
Text('Produit: ${sortie['produit_nom']}'),
const SizedBox(height: 16),
TextField(
controller: motifController,
decoration: const InputDecoration(
labelText: 'Motif du refus *',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (motifController.text.trim().isNotEmpty) {
Get.back(result: true);
} else {
Get.snackbar('Erreur', 'Veuillez indiquer un motif de refus');
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Refuser', style: TextStyle(color: Colors.white)),
),
],
),
);
if (confirm == true && motifController.text.trim().isNotEmpty) {
try {
await _database.refuserSortiePersonnelle(
sortie['id'] as int,
_userController.userId,
motifController.text.trim(),
);
Get.snackbar(
'Demande refusée',
'La sortie personnelle a été refusée',
backgroundColor: Colors.orange,
colorText: Colors.white,
);
_loadSortiesEnAttente();
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de refuser la demande: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'Approbation sorties personnelles'),
drawer: CustomDrawer(),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _loadSortiesEnAttente,
child: _sortiesEnAttente.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Aucune demande en attente',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _sortiesEnAttente.length,
itemBuilder: (context, index) {
final sortie = _sortiesEnAttente[index];
return _buildSortieCard(sortie);
},
),
),
);
}
Widget _buildSortieCard(Map<String, dynamic> sortie) {
final dateSortie = DateTime.parse(sortie['date_sortie'].toString());
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec statut
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'EN ATTENTE',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
),
const Spacer(),
Text(
DateFormat('dd/MM/yyyy HH:mm').format(dateSortie),
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 12),
// Informations du produit
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.inventory, color: Colors.blue.shade700, size: 16),
const SizedBox(width: 8),
Text(
'Produit demandé',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
],
),
const SizedBox(height: 8),
Text(
sortie['produit_nom'].toString(),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Row(
children: [
Text('Référence: ${sortie['produit_reference'] ?? 'N/A'}'),
const SizedBox(width: 16),
Text('Stock actuel: ${sortie['stock_actuel']}'),
const SizedBox(width: 16),
Text(
'Quantité demandée: ${sortie['quantite']}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
],
),
),
const SizedBox(height: 12),
// Informations du demandeur
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.person, color: Colors.green.shade700, size: 16),
const SizedBox(width: 8),
Text(
'Demandeur',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.green.shade700,
),
),
],
),
const SizedBox(height: 8),
Text(
'${sortie['admin_nom']} ${sortie['admin_nom_famille'] ?? ''}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (sortie['point_vente_nom'] != null)
Text('Point de vente: ${sortie['point_vente_nom']}'),
],
),
),
const SizedBox(height: 12),
// Motif
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.note, color: Colors.purple.shade700, size: 16),
const SizedBox(width: 8),
Text(
'Motif',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.purple.shade700,
),
),
],
),
const SizedBox(height: 8),
Text(
sortie['motif'].toString(),
style: const TextStyle(fontSize: 14),
),
if (sortie['notes'] != null && sortie['notes'].toString().isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'Notes: ${sortie['notes']}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontStyle: FontStyle.italic,
),
),
],
],
),
),
const SizedBox(height: 16),
// Vérification de stock
if ((sortie['stock_actuel'] as int) < (sortie['quantite'] as int))
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade300),
),
child: Row(
children: [
Icon(Icons.warning, color: Colors.red.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
'ATTENTION: Stock insuffisant pour cette demande',
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
const SizedBox(height: 16),
// Boutons d'action
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _refuserSortie(sortie),
icon: const Icon(Icons.close, size: 18),
label: const Text('Refuser'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: ((sortie['stock_actuel'] as int) >= (sortie['quantite'] as int))
? () => _approuverSortie(sortie)
: null,
icon: const Icon(Icons.check, size: 18),
label: const Text('Approuver'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
],
),
),
);
}
}